Buat APK Gratis

API jembatan JavaScript

appmintNative (aplikasi buatan AI)

Wrapper bertipe yang diimpor setiap proyek buatan AI dari @/lib/appmintNative. Masing-masing memilih jalur native di dalam aplikasi dan jalur web, atau jawaban none yang jujur, di browser - satu panggilan berfungsi di mana saja.

appmintNative.actionSheet appmintNative#

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

Contoh

Menampilkan lembar bawah "pilih salah satu" - kontrol native untuk menu tindakan sebuah baris.

Mengembalikan: Promise<number | null> - indeks opsi yang dipilih, atau null saat pengguna menutup lembar (ketuk latar, Escape, batal dari sistem). Penutupan itu normal, bukan kesalahan. Membutuhkan: tidak ada.

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

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

Buka dari tombol yang terlihat, dan dari tekan-lama pada baris:

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

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

Bentuk ala Capacitor ActionSheet.showActions({ title?, options }) menerima opsi yang sama dan menghasilkan { index }, dengan -1 untuk penutupan tanpa memilih:

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

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

Catatan:

  • Di aplikasi Android, plugin ActionSheet Capacitor menggambar lembar sistem; destructive memakai gaya destruktifnya.
  • Di tempat lain ini adalah sheet sungguhan dengan palet aplikasi (--surface, --ink, --danger), dengan latar belakang, Escape, fokus dipindah ke dalam sheet, dan getaran pemilihan.
  • Daftar options yang kosong langsung selesai dengan null. Untuk konten selain daftar tindakan, gunakan sheet().

appmintNative.ActionSheet appmintNative#

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

Contoh

Didokumentasikan bersama appmintNative.actionSheet - contoh di sana menunjukkan yang ini juga.

Menampilkan lembar bawah "pilih salah satu" - kontrol native untuk menu tindakan sebuah baris.

Mengembalikan: Promise<number | null> - indeks opsi yang dipilih, atau null saat pengguna menutup lembar (ketuk latar, Escape, batal dari sistem). Penutupan itu normal, bukan kesalahan. Membutuhkan: tidak ada.

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

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

Buka dari tombol yang terlihat, dan dari tekan-lama pada baris:

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

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

Bentuk ala Capacitor ActionSheet.showActions({ title?, options }) menerima opsi yang sama dan menghasilkan { index }, dengan -1 untuk penutupan tanpa memilih:

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

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

Catatan:

  • Di aplikasi Android, plugin ActionSheet Capacitor menggambar lembar sistem; destructive memakai gaya destruktifnya.
  • Di tempat lain ini adalah sheet sungguhan dengan palet aplikasi (--surface, --ink, --danger), dengan latar belakang, Escape, fokus dipindah ke dalam sheet, dan getaran pemilihan.
  • Daftar options yang kosong langsung selesai dengan null. Untuk konten selain daftar tindakan, gunakan sheet().

appmintNative.addContact appmintNative#

addContact(contact: Contact): Promise<boolean>

Contoh

Menyimpan kontak baru ke buku alamat ponsel. Hanya aplikasi Android: bridge shell menyimpannya langsung (tidak ada layar Kontak yang terbuka).

Mengembalikan: Promise<boolean> - true setelah tersimpan, false jika pengguna menolak izin kontak atau penyimpanan gagal. Ditolak dengan Error yang code-nya adalah 'not_enabled' (build tidak mengaktifkan penulisan kontak) atau 'foreign_origin', dan setelah menunggu 5 menit tanpa jawaban. Dibutuhkan: sakelar Edit Contacts di Langkah 4 (Access) - build mengaktifkannya saat kode Anda memanggil ini - dan izin pengguna saat pertama kali dipakai.

Simpan pelanggan dari sebuah formulir.

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

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

Lewati kontak yang sudah tersimpan. searchContacts membaca buku alamat, jadi periksa sebelum menambahkan:

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

export async function addIfNew(name: string, phone: string): Promise<'added' | 'exists' | 'refused' | 'app_only'> {
  if (can('contactAdd') === 'none') return 'app_only';
  const found = await searchContacts(name, 5);
  if (found.some((c) => c.phones?.includes(phone))) return 'exists';
  return (await addContact({ name, phones: [phone] })) ? 'added' : 'refused';
}

Bedakan kesalahan build dari penolakan. Penolakan dengan code 'foreign_origin' atau 'not_enabled' adalah masalah build, false adalah pilihan pengguna:

import { useState } from 'react';
import { addContact } from '@/lib/appmintNative';

export function SaveContactButton({ name, phone }: { name: string; phone: string }) {
  const [msg, setMsg] = useState('');
  async function onTap() {
    try {
      setMsg((await addContact({ name, phones: [phone] })) ? 'Saved.' : 'Contacts access was declined.');
    } catch (e) {
      const code = (e as { code?: string }).code;
      setMsg(code === 'foreign_origin' || code === 'not_enabled' ? 'This build cannot save contacts.' : 'Try again.');
    }
  }
  return <button onClick={onTap}>{msg || 'Save to contacts'}</button>;
}

Catatan: Ini langsung menyimpan - tidak ada editor kontak yang terbuka - jadi konfirmasikan dulu ke pengguna di layar Anda sendiri. Nomor telepon disimpan sebagai nomor seluler dan kontak disimpan di perangkat (tanpa akun). Menulis kontak bersifat sensitif: deklarasikan di formulir Data safety Play dan kebijakan privasi Anda.

appmintNative.alarmSupport appmintNative#

alarmSupport() — @/lib/appmintNative

Contoh

Apakah scheduleAlarm bisa mengatur alarm sekarang, dan apa yang kurang jika tidak.

Mengembalikan: 'ready', 'not_enabled' (dibuat tanpa Alarm clock), 'needs_exact_alarm' (Android 12+: alarm tepat waktu dimatikan), 'needs_notification_permission' (Android 13+), atau 'unavailable' (web dan iOS - jangan tampilkan kontrol alarm di sana). Dibutuhkan: 🔔 Alarm clock di langkah Akses (Langkah 4) saat Anda membuat aplikasi.

import { alarmSupport } from '@/lib/appmintNative';

export function AlarmSetupBanner() {
  const s = alarmSupport();
  if (s === 'ready' || s === 'unavailable') return null;
  const text = {
    not_enabled: 'This app was built without alarm clocks.',
    needs_exact_alarm: 'Allow "Alarms & reminders" for this app in Settings.',
    needs_notification_permission: 'Allow notifications so you can stop the alarm.',
  }[s];
  return <p role="alert">{text}</p>;
}

Atur alarm harian hanya saat ponsel siap, dan katakan alasannya jika tidak:

import { alarmSupport, scheduleAlarm } from '@/lib/appmintNative';

export function setMorningAlarm(hour: number, minute: number): string {
  const s = alarmSupport();
  if (s !== 'ready') return `Alarm not set (${s}).`;       // 'not_enabled', 'needs_exact_alarm', ...
  const first = new Date();
  first.setHours(hour, minute, 0, 0);
  if (first.getTime() <= Date.now()) first.setDate(first.getDate() + 1);
  const ok = scheduleAlarm('morning', { at: first.getTime(), title: 'Wake up', repeat: 'daily', snoozeMinutes: 5 });
  return ok ? 'Alarm set.' : 'The phone did not accept the alarm.';
}

Sembunyikan layar alarm di web dan bereaksi saat alarm berakhir, dikombinasikan dengan onAlarm:

import { useEffect, useState } from 'react';
import { alarmSupport, onAlarm, snoozeAlarm, stopAlarm } from '@/lib/appmintNative';

export function AlarmPanel() {
  const [ringing, setRinging] = useState<string | null>(null);
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open') setRinging(e.ringing ? e.id : null);
    else setRinging(null);                                  // dismiss, snooze, missed, error
  }), []);
  if (alarmSupport() === 'unavailable') return null;        // web and iOS: draw nothing
  if (!ringing) return null;
  return (
    <div role="alertdialog">
      <button onClick={() => stopAlarm(ringing)}>Stop</button>
      <button onClick={() => snoozeAlarm(ringing, 10)}>Snooze 10 min</button>
    </div>
  );
}

appmintNative.App appmintNative#

App.getInfo(): Promise<{ name, id, build, version }> / App.exitApp(): void / App.addListener(event, fn)

Contoh

Nama aplikasi, id paket, dan versi, cara menutup aplikasi, dan event aplikasi - dalam bentuk plugin App milik Capacitor.

Mengembalikan: getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → bentuk Capacitor (Promise berisi handle yang juga punya remove()). Event: appStateChange ({ isActive }), pause, resume (halaman masuk ke / kembali dari latar belakang), appUrlOpen ({ url }, deep link - sama dengan onDeepLink), backButton (penekanan atau gestur kembali; handler Anda yang memutuskan - aplikasi tidak ikut menutup). Membutuhkan: tidak ada.

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

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

Tutup aplikasi dari item "Keluar" yang bukan satu-satunya jalan maju:

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

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

Tampilkan versi aplikasi di layar React.

import { useEffect, useState } from 'react';
import { App } from '@/lib/appmintNative';

export function VersionLabel() {
  const [text, setText] = useState('');
  useEffect(() => {
    void App.getInfo().then((i) => setText(`${i.name} v${i.version}`));
  }, []);
  return <small>{text}</small>;
}

Catatan: Di aplikasi getInfo membaca plugin App Capacitor; jika tidak ada, memakai nama dan versi dari bridge shell (id dan build lalu menjadi ''), dan di web document.title dengan versi kosong. exitApp memakai closeWindow milik shell di aplikasi dan tidak melakukan apa-apa di web, jadi jangan jadikan Keluar satu-satunya jalan ke depan. Untuk event jeda/lanjut dan deep link lihat Capacitor.Plugins.App, launchUrl() dan onDeepLink().

appmintNative.appLauncher appmintNative#

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

Contoh

Membuka aplikasi lain lewat tautan - whatsapp://send?text=…, tel:, mailto:, geo:, atau tautan https yang diberikan OS ke aplikasinya - dan bertanya dulu apakah itu bisa berfungsi.

Mengembalikan: canOpen() → Promise<boolean>. open() → Promise<boolean> (false jika tidak ada aplikasi yang mengambil tautan). Dibutuhkan: tidak ada.

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

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

Telepon atau email (tidak perlu dicek; ponsel selalu punya aplikasi untuk ini):

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

await appLauncher.open('tel:+15551234567');
await appLauncher.open('mailto:[email protected]?subject=Order%2042');

Buka aplikasi peta dengan tautan geo:, dan beri tahu jika tidak ada yang mengambilnya:

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

export async function showOnMap(lat: number, lng: number, label: string) {
  const ok = await appLauncher.open(`geo:${lat},${lng}?q=${lat},${lng}(${encodeURIComponent(label)})`);
  if (!ok) showMessage('No maps app could open this place.');
}

Catatan: Di dalam aplikasi, ini memakai plugin AppLauncher dari Capacitor. Di Android 11+ canOpen hanya bisa melihat skema yang dideklarasikan aplikasi (https, geo, google.navigation, tel, mailto, sms, whatsapp, intent, upi); untuk yang lain jawabannya false meskipun aplikasinya terpasang - panggil open() dan periksa hasilnya. Di web, skema kustom tidak bisa diperiksa, jadi canOpen bernilai true hanya untuk http(s), dan open membuka tautan tersebut.

appmintNative.appLock appmintNative#

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

Contoh

Mengunci aplikasi lagi saat pengguna kembali setelah aplikasi berada di latar belakang beberapa saat: ini meminta sidik jari / wajah / PIN (unlock()), dan mengaktifkan layar privasi agar pengalih aplikasi tidak menampilkan konten Anda.

Mengembalikan: fungsi yang menghentikan kunci (panggil saat keluar akun atau unmount). onLocked(true) aktif saat aplikasi terkunci, onLocked(false) setelah berhasil dibuka. Membutuhkan: sama seperti unlock() - Android: Fingerprint dicentang saat Anda membuat build.

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

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

Di komponen root: const locked = useAppLock(); if (locked) return <LockScreen />;.

Hentikan kunci saat keluar akun, agar pengguna yang sudah keluar tidak diminta membuka kunci:

import { appLock } from '@/lib/appmintNative';

let stopLock: (() => void) | null = null;

export function onSignedIn() {
  stopLock = appLock({ timeoutSec: 120, title: 'Unlock your wallet' });
}

export function onSignedOut() {
  stopLock?.();            // removes the listener and turns the privacy screen off
  stopLock = null;
}

Tawarkan kunci hanya jika perangkat bisa membukanya, dan gabungkan dengan unlock() untuk tombol "Kunci sekarang" manual:

import { appLock, can, unlock } from '@/lib/appmintNative';

export function LockSettings({ onLockedChange }: { onLockedChange: (locked: boolean) => void }) {
  if (can('unlock') === 'none') return <p>This device has no screen lock to use.</p>;   // appLock would do nothing here
  return (
    <>
      <button onClick={() => appLock({ onLocked: onLockedChange })}>Turn on app lock</button>
      <button onClick={async () => { onLockedChange(true); if (await unlock('Unlock')) onLockedChange(false); }}>
        Lock now
      </button>
    </>
  );
}

Catatan: Jika perangkat tidak punya apa pun untuk membuka kunci (can('unlock') === 'none'), ini tidak melakukan apa-apa dan mengembalikan fungsi kosong - kunci aplikasi yang tidak pernah bisa dibuka akan menjebak pengguna. Jika pengguna membatalkan, ia meminta lagi hingga lima kali, lalu tetap terkunci (biarkan layar kunci Anda tampil) sampai aplikasi masuk ke latar belakang dan kembali. Ini terpisah dari opsi PIN Lock di wizard build, yang ditampilkan aplikasi secara native saat dibuka.

appmintNative.appUpdate appmintNative#

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

Contoh

Memeriksa Google Play untuk versi lebih baru dan memulai pembaruan dalam aplikasi dari Play (mengunduh dan memasang di tempat).

Mengembalikan: check() → Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null di web, tanpa Play services, atau saat Play gagal). updateAvailable adalah jawaban toko sendiri. start() → Promise<boolean>: true hanya jika pembaruan Play selesai, false jika pengguna membatalkan, gagal, atau tidak ada yang tersedia. Dibutuhkan: tidak ada; aplikasi harus dipasang dari Google Play.

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

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

Bandingkan version code Play dengan yang terinstal, dan anggap null sebagai "tidak bisa diperiksa di sini":

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

export async function updateStatus(): Promise<string> {
  const info = await appUpdate.check();
  if (!info) return 'Update check is not available here.';     // web, no Play services, or Play failed
  if (!info.updateAvailable) return `You have the latest version (${info.currentVersion}).`;
  return `Update available: version code ${info.availableVersion} (you have ${info.currentVersionCode}).`;
}

Banner React yang memeriksa sekali saat layar terbuka:

import { useEffect, useState } from 'react';
import { appUpdate, type AppUpdateInfo } from '@/lib/appmintNative';

export function UpdateBanner() {
  const [info, setInfo] = useState<AppUpdateInfo | null>(null);
  useEffect(() => { void appUpdate.check().then(setInfo); }, []);
  if (!info?.updateAvailable) return null;
  return <button onClick={() => void appUpdate.start()}>Update now</button>;
}

Catatan: updateAvailable adalah jawaban Play sendiri; inPlace menambahkan bahwa Play bisa menjalankan pembaruan dalam aplikasinya sekarang juga. availableVersion adalah KODE versi Play (Play tidak memberi nama) - bandingkan dengan currentVersionCode, jangan dengan currentVersion, yaitu nama versi.

appmintNative.askForReview appmintNative#

askForReview(options): Promise<ReviewOutcome>

Contoh

Meminta Google Play menampilkan lembar penilaiannya sendiri di dalam aplikasi dan melaporkan apa yang terjadi.

Mengembalikan: Promise<ReviewOutcome> - { result, openedStore, message? } dengan result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' di web). { openStore: true } membuka listing Play jika lembar ulasan tidak muncul. Membutuhkan: tidak ada; lembar ini hanya muncul untuk aplikasi yang diinstal dari Google Play.

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

export async function onRateTap() {
  const r = await askForReview({ openStore: true });   // listing if Play shows no sheet
  if (r.result === 'shown' || r.openedStore) toast('Thanks for rating!');
}

Beri tahu pengguna apa yang terjadi untuk setiap hasil:

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

export async function rateMessage(): Promise<string> {
  const r = await askForReview();
  switch (r.result) {
    case 'shown': return 'Thanks for rating!';
    case 'not_from_play': return 'Rating works for installs from Google Play.';
    case 'quota': return 'Google limits how often the sheet shows. Try again later.';
    case 'unavailable': return '';                       // the web: draw no rating button at all
    default: return `Could not open the rating sheet${r.message ? `: ${r.message}` : ''}.`;
  }
}

Tanya sekali saja, setelah pengguna menyelesaikan tugas ketiganya, dan ingat bahwa Anda sudah bertanya:

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

export async function maybeAskAfterTask(completed: number) {
  if (completed !== 3 || localStorage.getItem('asked-review')) return;
  localStorage.setItem('asked-review', '1');
  await askForReview();
}

Catatan: Minta setelah momen sukses dan jarang-jarang. Instalasi uji sideload selalu menjawab not_from_play.

appmintNative.backgroundLocation appmintNative#

import { backgroundLocation } from '@/lib/appmintNative'

Contoh

Lokasi yang terus datang saat aplikasi di latar belakang: start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.

Mengembalikan: start(options) → Promise<BgResult> ({ ok, background, backgroundPermission, precise } atau { ok: false, reason }). stop / clearBuffered → Promise<boolean>. buffered() → Promise<LocationFix[]>. requestAlways() → Promise<{ foreground, background, precise, error? }>. onFix(cb) → fungsi berhenti berlangganan; cb menerima setiap detail appmint:location. Dibutuhkan: Background location di build; izin lokasi (minta dengan navigator.geolocation lebih dulu).

import { backgroundLocation, type LocationFix } from '@/lib/appmintNative';

export async function startRun(onPoint: (p: LocationFix) => void) {
  const r = await backgroundLocation.start({ intervalMs: 5000, minDistanceM: 5, accuracy: 'high',
    uploadUrl: 'https://api.example.com/run-points' });
  if (!r.ok) throw new Error(r.reason);
  const off = backgroundLocation.onFix((d) => { if (d.type === 'fix') onPoint(d); });
  // Points recorded while the screen was off:
  (await backgroundLocation.buffered()).forEach(onPoint);
  await backgroundLocation.clearBuffered();
  return async () => { off(); await backgroundLocation.stop(); };
}

Minta "Izinkan sepanjang waktu" sebelum memulai, dan jelaskan penolakan:

import { backgroundLocation } from '@/lib/appmintNative';

export async function beginTracking(): Promise<string> {
  if (backgroundLocation.available() === 'none') return 'Background location works in the app only.';
  const p = await backgroundLocation.requestAlways();
  if (p.error) return `Could not ask for permission (${p.error}).`;
  if (p.background !== 'granted') return 'Choose "Allow all the time" to keep tracking with the screen off.';
  const r = await backgroundLocation.start({ intervalMs: 10000, accuracy: 'balanced' });
  return r.ok ? 'Tracking.' : `Could not start (${r.reason}).`;
}

Hook React yang menampilkan poin langsung dan berhenti mendengarkan saat layar ditutup:

import { useEffect, useState } from 'react';
import { backgroundLocation, type LocationFix } from '@/lib/appmintNative';

export function useLastFix(): LocationFix | null {
  const [fix, setFix] = useState<LocationFix | null>(null);
  useEffect(() => backgroundLocation.onFix((d) => {
    if (d.type === 'fix') setFix(d);          // { type: 'error', reason } events are skipped here
  }), []);                                    // onFix returns its unsubscribe
  return fix;
}

appmintNative.backgroundSync appmintNative#

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

Contoh

Permintaan HTTP native yang berjalan saat aplikasi tertutup: enqueue (sekali jalan, simpan otomatis yang tahan lama dan dikirim saat online), schedule (permintaan berulang, minimal setiap 15 menit), cancel dan list.

Mengembalikan: enqueue / schedule → Promise<boolean>; cancel → Promise<void>; list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } - pendingDelivery di Android: satu kali yang belum terkirim). Membutuhkan: tidak ada di aplikasi Android (memakai WorkManager). Di web can('backgroundSync') bernilai 'none' dan enqueue / schedule selesai dengan false.

Simpan otomatis yang tahan lama: ID yang sama menggantikan simpanan yang belum dikirim.

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

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

Sinkronisasi berulang, daftarkan dan batalkan:

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

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

await backgroundSync.cancel('sync');

Event mentah (yang ditunggu wrapper di Android):

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

Catatan: true berarti tugas sudah diantrekan, belum terkirim. false berarti tidak diantrekan: shell menjawab status: 'failed' (URL tidak diawali http, id kosong, atau sistem menolak) atau tidak menjawab dalam 10 detik. JavaScript halaman tidak pernah berjalan di latar belakang; hanya permintaan ini yang berjalan. Balasan 4xx tidak dicoba ulang; kesalahan jaringan dan 5xx dicoba ulang hingga 8 kali.

appmintNative.backgroundTasks appmintNative#

import { backgroundTasks } from '@/lib/appmintNative'

Contoh

Kode halaman itu sendiri, dijalankan secara berkala saat aplikasi ditutup (wrapper bertipe di atas AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.

Mengembalikan: register / runNow → Promise<BgResult>; unregister / done → Promise<boolean>; list() → tugas terdaftar dengan lastRunAt / lastResult; isBackgroundRun() / currentRun() bersifat sinkron. Dibutuhkan: Run page code while closed di build.

import { backgroundTasks, notify } from '@/lib/appmintNative';

// In the app's entry file, before rendering:
if (backgroundTasks.isBackgroundRun()) {
  backgroundTasks.onRun(async (run) => {
    try {
      const unread = await fetch('/api/unread').then((r) => r.json());
      if (unread.count > 0) await notify({ title: 'New messages', body: `${unread.count} unread` });
      await backgroundTasks.done(run.runId, { ok: true });
    } catch {
      await backgroundTasks.done(run.runId, { ok: false, retry: true });
    }
  });
} else {
  void backgroundTasks.register('unread', { intervalMinutes: 30, requiresNetwork: true });
  // renderApp();
}

Daftarkan saat masuk, hapus saat keluar, dan daftar apa yang terdaftar:

import { backgroundTasks } from '@/lib/appmintNative';

export async function onSignedIn() {
  const r = await backgroundTasks.register('sync', { intervalMinutes: 60, requiresNetwork: true });
  if (!r.ok) console.warn('Background sync not set up:', r.reason);   // 'not_enabled' or 'unsupported'
}

export async function onSignedOut() {
  await backgroundTasks.unregister('sync');
}

export async function taskCountText(): Promise<string> {
  const tasks = await backgroundTasks.list();
  return tasks.length ? `Registered tasks: ${tasks.length}` : 'No background tasks.';
}

Jalankan tugas sekarang dari tombol "Sinkronkan sekarang" dan periksa jawabannya:

import { backgroundTasks } from '@/lib/appmintNative';

export function SyncNowButton() {
  return (
    <button
      disabled={backgroundTasks.available() === 'none'}
      onClick={async () => {
        const r = await backgroundTasks.runNow('sync');
        if (!r.ok) alert(`Could not run it (${r.reason}).`);
      }}
    >
      Sync now
    </button>
  );
}

appmintNative.badge appmintNative#

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

Contoh

Mengatur atau menghapus jumlah belum dibaca pada ikon peluncur aplikasi.

Mengembalikan: badge.set(count) → Promise<boolean> (false jika plugin Badge tidak ada, mis. di web); badge.clear() → Promise<void>. Membutuhkan: tidak ada di Android (memakai plugin Badge bawaan).

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

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

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

Cerminkan jumlah belum dibaca saat pesan push tiba ketika aplikasi sedang terbuka:

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

export function watchInbox(): () => void {
  const refresh = () => void badge.set(pushInbox().filter((m) => !m.read).length);
  window.addEventListener('appmint:push', refresh);
  refresh();
  return () => window.removeEventListener('appmint:push', refresh);
}

Tandai semuanya sudah dibaca dan hapus hitungan sekaligus; lewati badge di tempat yang tidak ada:

import { badge, can, markPushRead } from '@/lib/appmintNative';

export async function markAllRead(): Promise<boolean> {
  markPushRead();                              // no id = every message
  if (can('badge') === 'none') return false;   // the web: nothing to clear
  await badge.clear();
  return true;
}

Catatan: di Android tampilannya bergantung pada launcher: banyak yang menampilkan angka, sebagian hanya titik, sebagian tidak menampilkan apa-apa - jangan taruh informasi hanya di badge. Di web can('badge') adalah 'none'.

appmintNative.barcodeFormats appmintNative#

barcodeFormats(): Promise<string[]>

Contoh

Mencantumkan format barcode dan QR yang bisa dibaca BarcodeDetector perangkat ini.

Mengembalikan: Promise<string[]> - nama format seperti 'qr_code', 'ean_13', 'code_128'; array kosong jika pembacaan barcode tidak tersedia di sini. Tidak pernah menolak. Membutuhkan: tidak ada.

Tampilkan opsi "Pindai barcode produk" hanya jika perangkat membaca format itu:

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

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

Persempit pemindaian ke apa yang didukung perangkat:

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

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

Buat pemilih format yang hanya menampilkan apa yang bisa dibaca perangkat ini:

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

export function FormatPicker({ onPick }: { onPick: (format: string) => void }) {
  const [formats, setFormats] = useState<string[]>([]);
  useEffect(() => { void barcodeFormats().then(setFormats); }, []);
  if (formats.length === 0) return <p>Barcode reading is not available on this device.</p>;   // [] = none
  return (
    <select onChange={(e) => onPick(e.target.value)}>
      {formats.map((f) => <option key={f} value={f}>{f}</option>)}
    </select>
  );
}

Catatan:

  • Ini menanyakan BarcodeDetector.getSupportedFormats() milik mesin browser. WebView aplikasi Android memilikinya (terukur: aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e); banyak browser desktop dan Safari tidak, dan menjawab [].
  • Daftar ini tentang readBarcodeFrom() / scanBarcode(). scanCode() di aplikasi memakai pemindai Google dan tidak bergantung padanya.

appmintNative.batteryOptimization appmintNative#

import { batteryOptimization } from '@/lib/appmintNative'

Contoh

Pengecualian optimasi baterai: isIgnoring() dan request().

Mengembalikan: isIgnoring() → boolean. request() → Promise<string>: 'already_ignoring', 'dialog_opened' (opsi build "Ask to skip battery optimisation directly"), 'settings_opened', 'unavailable', atau 'unsupported' di luar aplikasi Android. Dibutuhkan: tidak ada untuk daftar pengaturan.

import { batteryOptimization } from '@/lib/appmintNative';

export async function makeReliable() {
  if (batteryOptimization.isIgnoring()) return true;
  const how = await batteryOptimization.request();
  if (how === 'settings_opened') alert('Choose this app in the list and set it to "Not optimised".');
  return false;
}

Tampilkan status saat ini pada baris pengaturan:

import { useState } from 'react';
import { batteryOptimization } from '@/lib/appmintNative';

export function BatteryRow() {
  const [ignoring, setIgnoring] = useState(batteryOptimization.isIgnoring());
  return (
    <button
      disabled={ignoring}
      onClick={async () => {
        const how = await batteryOptimization.request();
        if (how === 'unsupported' || how === 'unavailable') alert('This phone has no setting for it.');
        setIgnoring(batteryOptimization.isIgnoring());   // reads the current answer after the dialog or settings screen
      }}
    >
      {ignoring ? 'Battery limits are off' : 'Allow background work'}
    </button>
  );
}

Minta pengecualian sebelum memulai pekerjaan latar belakang yang panjang, bersama dengan keepAlive:

import { batteryOptimization, keepAlive } from '@/lib/appmintNative';

export async function startUpload() {
  if (!batteryOptimization.isIgnoring()) await batteryOptimization.request();
  const r = await keepAlive.start({ title: 'Uploading photos', type: 'dataSync' });
  if (!r.ok) throw new Error(r.reason);
}

appmintNative.bleSensors appmintNative#

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

Contoh

Sensor kebugaran Bluetooth standar - detak jantung, kecepatan/kadensi bersepeda, daya, kecepatan/kadensi lari, dan baterai sensor - dengan angka yang sudah diurai, di aplikasi Android.

Mengembalikan: setiap metode mengembalikan Promise<void> (setWheelCircumference mengembalikan void). Semua yang lain tiba sebagai event window appmint:ble dengan detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. Membutuhkan: Bluetooth, yang diaktifkan build sendiri saat kode memakai bleSensors. Di browser (pratinjau web) tidak ada bridge sensor: startScan() menjawab dengan {kind:'error', error:'not_enabled'}, jadi tangani kesalahan itu. Untuk mengetahui di awal, uji can('bluetooth') === 'native' - bukan !== 'none': di Chrome can('bluetooth') adalah 'web' (Web Bluetooth), yang tidak dipakai bleSensors.

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

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

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

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

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

Batasi layar pada aplikasi yang terpasang.

import { bleSensors, can } from '@/lib/appmintNative';

export function SensorsEntry() {
  // 'native' only: in Chrome can('bluetooth') is 'web', which bleSensors does not use.
  if (can('bluetooth') !== 'native') return <p>Heart-rate sensors work in the installed app.</p>;
  return <button onClick={() => void bleSensors.startScan()}>Find sensors</button>;
}

Hentikan pemindaian pada sensor pertama dan pantau kecepatan bersepeda.

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

type BikeEvent = { kind: string; address?: string; type?: string; data?: { speedKmh?: number } };

export function BikeSpeed() {
  const [kmh, setKmh] = useState<number | null>(null);

  useEffect(() => {
    bleSensors.setWheelCircumference(2096);   // 700x23c
    const onBle = (e: Event) => {
      const d = (e as CustomEvent<BikeEvent>).detail;
      if (d.kind === 'device' && d.address) {
        void bleSensors.stopScan().then(() => bleSensors.connect(d.address!));
      } else if (d.kind === 'data' && d.type === 'speed_cadence') {
        setKmh(d.data?.speedKmh ?? null);
      }
    };
    window.addEventListener('appmint:ble', onBle);
    void bleSensors.startScan();
    return () => { window.removeEventListener('appmint:ble', onBle); void bleSensors.disconnect(); };
  }, []);

  return <p>{kmh === null ? 'Looking for a sensor...' : `${kmh.toFixed(1)} km/h`}</p>;
}

Catatan: Pemindaian berlangsung 15 detik (stopScan() mengakhirinya lebih awal); satu sensor terhubung dalam satu waktu. Kode kesalahan: not_enabled, permission_denied, bluetooth_unavailable, bluetooth_off, scan_failed, invalid_address. Kecepatan membutuhkan dua paket dan ukuran roda (bawaan 2096 mm). Pesan yang sama juga dikirim ke window.onAppMintBle; mendengarkan event lebih sederhana. Untuk perangkat Bluetooth lainnya, gunakan navigator.bluetooth.

appmintNative.brightness appmintNative#

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

Contoh

Menaikkan kecerahan layar untuk boarding pass atau kode QR, lalu mengembalikan kendali ke sistem. Hanya native - halaman web tidak bisa mengendalikan lampu latar.

Mengembalikan: get() → Promise<number | null> (0-1 saat aplikasi menimpa level; -1 saat aplikasi mengikuti level sistem; null di web atau saat tidak bisa dibaca). set(value) → Promise<boolean> (nilai dibatasi ke 0-1; false di web). restore() → Promise<void>. Membutuhkan: tidak ada.

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

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

Ingat levelnya, terangkan, lalu kembalikan.

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

export async function showBoardingPass(render: () => Promise<void>) {
  const before = await brightness.get();   // -1 = following the system, null = not readable here
  const ok = await brightness.set(1);
  try {
    await render();
  } finally {
    if (ok) await brightness.restore();    // returns to the system level
  }
  return before;
}

Jaga layar tetap terang dan menyala saat kode ditampilkan.

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

export function useQrPresenting(active: boolean) {
  useEffect(() => {
    if (!active) return;
    if (can('brightness') !== 'none') void brightness.set(0.95);
    void keepAwake(true);
    return () => { void brightness.restore(); void keepAwake(false); };
  }, [active]);
}

Catatan: Ini hanya mengubah jendela aplikasi ini, bukan pengaturan kecerahan ponsel, dan penimpaan berakhir saat aplikasi ditutup. restore() mengatur -1 (ikuti sistem). get() membaca penimpaan jendela, jadi nilainya -1 sampai Anda memanggil set() - itu bukan level sistem.

appmintNative.browser appmintNative#

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

Contoh

Membuka halaman di tab browser dalam aplikasi (Chrome Custom Tabs) - untuk ketentuan, halaman bantuan, atau halaman masuk - dan tab baru di web.

Mengembalikan: open() → Promise<boolean> (false jika tab tidak bisa dibuka). close() → Promise<void>. onClosed(cb) → fungsi untuk berhenti berlangganan; cb dijalankan saat pengguna menutup tab (hanya di aplikasi). Membutuhkan: tidak ada.

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

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

Tautan ketentuan di komponen React.

import { browser, toast } from '@/lib/appmintNative';

export function TermsLink() {
  return (
    <button
      onClick={async () => {
        if (!(await browser.open('https://example.com/terms'))) toast('Could not open the terms page');
      }}
    >
      Terms of use
    </button>
  );
}

Tutup tab dari kode dan segarkan saat pengguna kembali.

import { useEffect } from 'react';
import { browser } from '@/lib/appmintNative';

export function usePaymentTab(onBack: () => void) {
  useEffect(() => browser.onClosed(onBack), [onBack]);   // the returned function unsubscribes
  return {
    start: (url: string) => browser.open(url),
    cancel: () => browser.close(),   // closes the tab in the app; does nothing on the web
  };
}

Catatan: toolbarColor secara default memakai token --bg milik aplikasi. Tab ini adalah browser terpisah - tidak berbagi cookie atau localStorage dengan aplikasi. close() menutup tab dari kode di dalam aplikasi; di web tidak berpengaruh. onClosed tidak pernah aktif di web.

appmintNative.calendar appmintNative#

calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar

Contoh

Menambah, mendaftar, dan menghapus acara di kalender ponsel pengguna SENDIRI (penyedia kalender Android), dan membuka aplikasi kalender pada tanggal tertentu.

Mengembalikan: requestPermission() → Promise<'granted' | 'denied'>. createEvent(input) → Promise<string | null> (ID event; null jika pengguna menolak akses kalender - ini menolak jika event itu sendiri tidak bisa dibuat, misalnya tanggal tidak valid). listEvents({ from, to }) → Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (waktu dalam epoch ms). deleteEvent(id) / openCalendar(date?) → Promise<boolean>. Hanya createEvent yang menolak, dan hanya untuk event yang tidak bisa dibuat. Membutuhkan: aktifkan Kalender di Langkah 4 (Akses) saat Anda membuat build - build mencentangnya untuk Anda saat melihat calendar.createEvent(, calendar.listEvents( atau calendar.requestPermission( di kode.

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

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

Daftarkan minggu ini, hapus, buka:

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

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

Tambahkan ke kalender hanya jika penulisan memungkinkan.

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

export async function addReminder(title: string, when: Date) {
  if (can('calendarWrite') === 'none') { toast('Calendar is not available here'); return null; }
  const id = await calendar.createEvent({ title, start: when, end: new Date(when.getTime() + 30 * 60_000) });
  toast(id ? 'Added to your calendar' : 'Calendar access was refused');
  return id;
}

Catatan: Di web, createEvent mengunduh file .ics yang dibuka pengguna di kalendernya (mengembalikan UID-nya), listEvents mengembalikan [] (can('calendarRead') adalah 'none'), dan deleteEvent / openCalendar mengembalikan false. Pengingat dan event berulang tersedia di Capacitor.Plugins.Calendar, tidak melalui wrapper ini.

appmintNative.camera appmintNative#

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

Contoh

Mengambil foto dengan kamera atau memilih foto dari galeri: plugin Capacitor Camera di aplikasi, input file dengan capture di web.

Mengembalikan: take() → Promise<Photo | null> dengan { dataUrl, mime }; null jika dibatalkan atau izin ditolak. pick() → Promise<Photo[]>, kosong jika dibatalkan. Membutuhkan: Camera di Langkah 4 (Akses) - build mengaktifkannya saat kode Anda memanggil camera.take.

Ambil foto profil dengan kamera depan.

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

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

Pilih satu foto dari galeri dengan take, atau beberapa dengan pick.

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

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

Unggah foto dengan mengubah data URL menjadi Blob.

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

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

Catatan: Opsi: source ('camera' bawaan, atau 'photos'), quality 0-100 (bawaan 80), direction ('rear' bawaan, atau 'front'), allowEditing. Foto tidak disimpan ke galeri. Izin yang ditolak diselesaikan dengan null / [], tidak pernah melempar kesalahan - tampilkan petunjuk singkat agar pengguna tahu mengapa tidak ada yang terjadi. Kamera bersifat sensitif: deklarasikan di formulir Data safety Play Anda.

appmintNative.can appmintNative#

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

Contoh

Memberi tahu di mana fitur benar-benar akan berjalan pada permukaan saat ini, sehingga aplikasi hanya menampilkan kontrol yang berfungsi.

Mengembalikan: sebuah string, secara sinkron: 'native' (implementasi aplikasi sendiri - bridge shell atau plugin Capacitor yang tertanam di aplikasi), 'web' (standar web asli di browser ini) atau 'none' (tidak tersedia di sini - jangan tampilkan kontrolnya). Membutuhkan: tidak ada.

Sembunyikan kontrol yang tidak bisa berfungsi di permukaan ini:

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

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

Ubah kata-katanya berdasarkan permukaan:

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

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

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

Tampilkan petunjuk alih-alih kontrol yang mati, dan ubah fitur menjadi ya/tidak sederhana dengan has():

import { can, has, keepAwake } from '@/lib/appmintNative';

export function KeepScreenOn({ on }: { on: boolean }) {
  if (!has('keepAwake')) return <p>Keep-awake is not available on this device.</p>;
  return <label><input type="checkbox" checked={on} onChange={(e) => void keepAwake(e.target.checked)} /> Keep screen on</label>;
}

export const scansInApp = can('codeScanner') === 'native';

Catatan:

  • Plugin Capacitor yang tertanam di aplikasi selalu menjawab 'native'; jika tidak, bridge shell diperiksa, lalu standar web.
  • toast, actionSheet, dialog, statusBar dan swipe memiliki versi web asli, jadi nilainya bukan 'none' di browser.
  • contactList, contactAdd, smsSend, smsList, callLog, shortcuts, shareTarget dan widget bernilai 'none' di web.
  • Beberapa fitur hanya ada jika build mengaktifkannya (akses folder, kunci aman, kontrol media, SMS, kontak). Tanyakan ke can(), bukan isNativeApp().
  • Nama yang tidak dikenal mengembalikan 'none'. Untuk jawaban ya/tidak sederhana gunakan has(cap).

appmintNative.clearPushInbox appmintNative#

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

Contoh

Mengosongkan kotak masuk AppMint Push di ponsel ini. Permanen - tidak ada urungkan.

Mengembalikan: boolean secara sinkron - true jika dibersihkan; selalu false di web. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat build.

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

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

Tampilkan Hapus hanya jika ada yang bisa dihapus, dan laporkan hasilnya:

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

export function InboxToolbar({ onChange }: { onChange: () => void }) {
  const count = pushInbox().length;
  if (count === 0) return null;
  return (
    <button
      onClick={() => {
        // false on the web, or in an app built without AppMint Push
        if (clearPushInbox()) { toast('Inbox cleared'); onChange(); }
        else toast('Could not clear the inbox here');
      }}
    >
      Clear all ({count})
    </button>
  );
}

Tandai semuanya sudah dibaca alih-alih menghapusnya, sehingga tidak ada yang hilang:

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

export function markAllRead(): number {
  const unread = pushInbox().filter((m) => !m.read).length;
  markPushRead();   // no argument = every message
  return unread;
}

Catatan: notifikasi di baki dan riwayat pengiriman Anda di AppMint tidak tersentuh; pesan baru tetap tiba.

appmintNative.clearShared appmintNative#

clearShared(): void

Contoh

Menghapus teks atau tautan yang dibagikan aplikasi lain ke aplikasi ini, setelah aplikasi Anda menyimpannya.

Mengembalikan: tidak ada. Dibutuhkan: tidak ada (tidak melakukan apa-apa di web).

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

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

Hapus bagikan hanya setelah tersimpan, dan pertahankan saat penyimpanan gagal:

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

export async function saveSharedLink(save: (url: string) => Promise<void>) {
  const p = sharedText();
  if (!p?.url) return;
  try {
    await save(p.url);
    clearShared();
    toast('Saved');
  } catch {
    toast('Could not save, the shared link is still waiting');   // not cleared, so it is not lost
  }
}

Tangani kiriman berbagi yang masuk saat aplikasi sedang terbuka:

import { useEffect } from 'react';
import { onShared, clearShared } from '@/lib/appmintNative';

export function useIncomingShare(addNote: (text: string) => void) {
  useEffect(() => onShared((p) => {
    addNote(p.subject ? `${p.subject}\n${p.text}` : p.text);
    clearShared();
  }), [addNote]);
}

Catatan: sharedText() menyimpan kiriman berbagi sampai ini dipanggil, memang disengaja, agar layar yang dimuat belakangan tetap melihatnya. Panggil clearShared() hanya SETELAH kiriman itu tersimpan dengan aman - jika penyimpanan gagal, biarkan agar pengguna tidak kehilangannya.

appmintNative.click appmintNative#

click() — @/lib/appmintNative

Contoh

Memutar bunyi ketuk aplikasi sekali, untuk tombol Anda sendiri.

Mengembalikan: void. Dibutuhkan: Tap Sound diaktifkan di Langkah 3 (Integrate) saat Anda membuat aplikasi. Di web, dan di aplikasi yang dibuat tanpa Tap Sound, tidak melakukan apa-apa.

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

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

Biarkan pengguna mematikan suara di pengaturan Anda sendiri:

import { click } from '@/lib/appmintNative';

export function useTapSound(enabled: boolean) {
  return () => { if (enabled) click(); };   // a no-op on the web and in apps built without Tap Sound
}

Putar dari baris daftar bersama toast:

import { click, toast } from '@/lib/appmintNative';

export function Row({ name, onPick }: { name: string; onPick: () => void }) {
  return <li onClick={() => { click(); onPick(); toast(`${name} selected`); }}>{name}</li>;
}

Catatan: Dengan Follow phone setting (recommended) bunyi klik senyap jika pengguna mematikan "Touch sounds" di ponsel. Dengan Always play on every tap, setiap ketukan sudah berbunyi klik, jadi panggilan click() tambahan tidak diperlukan. Hanya Android (milik shell WebToApk.playClick).

appmintNative.Clipboard appmintNative#

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

Contoh

Clipboard dalam bentuk plugin Clipboard Capacitor - kode yang sama dengan copy() / paste().

Mengembalikan: write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value adalah '' saat clipboard kosong). Membutuhkan: tidak ada.

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

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

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

Salin kode dan konfirmasi dengan toast:

import { Clipboard, toast } from '@/lib/appmintNative';

export async function copyCode(code: string) {
  await Clipboard.write({ string: code });
  toast('Code copied');
}

Tulis tautan jika tidak ada teks, dan perlakukan clipboard kosong sebagai tidak ada yang bisa ditempel:

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

export async function copyUrl(url: string) {
  await Clipboard.write({ url });   // used only because string is empty
}

export async function readCode(): Promise<string | null> {
  try {
    const { value } = await Clipboard.read();
    return value.trim() || null;    // '' = the clipboard is empty
  } catch {
    return null;                    // the browser denied access
  }
}

Catatan: Di aplikasi memakai plugin Clipboard Capacitor; di browser navigator.clipboard. write hanya mengirim string (atau url jika string kosong) - tanpa gambar. Di browser, read() bisa melempar kesalahan saat pengguna menolak akses; di aplikasi clipboard kosong adalah '', bukan kesalahan. Tanyakan can('clipboardRead') sebelum menampilkan kontrol Tempel.

appmintNative.collapsingHeader appmintNative#

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

Contoh

Membuat header berjudul besar bereaksi terhadap gulir seperti yang asli, dengan menerbitkan nilai 0 → 1 yang bisa dipakai CSS Anda.

Mengembalikan: fungsi berhenti berlangganan yang juga menghapus variabel dan atributnya. Membutuhkan: tidak ada.

Ini mengatur --collapse (dari 0 di atas hingga 1 setelah gulir range px) pada header, dan menambahkan data-collapsed saat sudah sepenuhnya menciut:

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

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

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

  return (
    <div className="flex h-full flex-col">
      <header ref={headerRef} className="app-header">Notes</header>
      <div ref={listRef} className="flex-1 overflow-y-auto">{/* rows */}</div>
    </div>
  );
}
.app-header { font-size: calc(28px - 10px * var(--collapse, 0)); padding-top: var(--safe-top); }
.app-header[data-collapsed] { box-shadow: 0 1px 0 var(--border); }

Berhenti mendengarkan dari skrip biasa; ini juga menghapus --collapse dan data-collapsed:

import { collapsingHeader } from '@/lib/appmintNative';

const header = document.querySelector<HTMLElement>('.app-header')!;
const stop = collapsingHeader(header);   // whole-page scroll, default 64 px range
addEventListener('pagehide', stop);

Catatan:

  • scroller bawaan adalah window (halaman yang digulir seluruhnya); range bawaan adalah 64 px.
  • Listener scroll bersifat pasif dan memperbarui sekali per frame animasi, sehingga tidak memperlambat scroll.

appmintNative.composeSms appmintNative#

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

Contoh

Membuka aplikasi pesan dengan nomor dan teks yang sudah terisi; pengguna menekan kirim. Berfungsi di aplikasi Android (bridge shell) dan di web (tautan sms:).

Mengembalikan: boolean segera - true jika aplikasi pesan diminta untuk dibuka. Membutuhkan: tanpa izin.

Tombol "Kirim SMS ke toko".

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

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

Hook yang bisa dipakai ulang yang menyembunyikan tombol saat tidak ada angka.

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

export function useTextShop(phone: string) {
  const available = can('smsCompose') !== 'none' && phone.trim() !== '';
  return {
    available,
    send: (orderId: string) => composeSms(phone, `Hi, about order ${orderId}.`),
  };
}

Undang teman; pengguna menekan kirim.

import { composeSms, toast } from '@/lib/appmintNative';

export function inviteFriend(number: string, link: string) {
  if (!composeSms(number, `Join me: ${link}`)) toast('Could not open the messaging app');
}

Catatan: Pilih ini daripada sendSms() untuk apa pun yang ditulis atau disetujui pengguna - tidak ada yang keluar dari ponsel tanpa pengguna menekan kirim. Di web ini membuka sms:…?body=…. Panggilan composeSms( tidak mengaktifkan SMS (read & send) saat build, jadi tidak menambah izin yang dibatasi Play.

appmintNative.copy appmintNative#

copy(text: string): Promise<boolean>

Contoh

Menaruh teks ke clipboard sistem.

Mengembalikan: Promise<boolean> - true jika teks berhasil disalin, false jika clipboard tidak tersedia atau browser menolak. Dibutuhkan: tidak ada.

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

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

Bentuk ala Capacitor melakukan hal yang sama:

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

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

Laporkan kedua hasil, dengan haptic hanya saat berhasil:

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

export async function copyCoupon(code: string) {
  const ok = await copy(code);
  if (ok) haptic('success');
  toast(ok ? `Copied ${code}` : 'Copy failed, press and hold to copy');
  return ok;
}

Catatan: Di aplikasi, plugin Capacitor Clipboard menulis teksnya (kesalahan plugin menolak promise, bukan mengembalikan false). Di browser, ini memakai navigator.clipboard.writeText, yang membutuhkan halaman https dan biasanya ketukan pengguna.

appmintNative.copyImage appmintNative#

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

Contoh

Menyalin gambar ke clipboard agar bisa ditempel di aplikasi chat dan email: clipboard aplikasi di aplikasi Android, Async Clipboard API di web.

Mengembalikan: Promise<boolean> - true setelah tersalin. Membutuhkan: tidak ada; batasi kontrolnya dengan can('clipboardImage').

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

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

Salin foto dari kamera, lalu konfirmasikan dengan toast.

import { camera, copyImage, toast } from '@/lib/appmintNative';

export async function copySnapshot() {
  const photo = await camera.take({ source: 'camera' });
  if (!photo) return;                                  // the user cancelled
  const blob = await (await fetch(photo.dataUrl)).blob();
  toast((await copyImage(blob)) ? 'Photo copied' : 'Could not copy the photo');
}

Tampilkan tombol hanya jika clipboard menerima gambar, dan salin gambar dari URL.

import { useEffect, useState } from 'react';
import { can, copyImage, toast } from '@/lib/appmintNative';

export function CopyLogoButton({ src }: { src: string }) {
  const [ok, setOk] = useState(false);
  useEffect(() => { setOk(can('clipboardImage') !== 'none'); }, []);
  if (!ok) return null;
  return (
    <button onClick={async () => {
      const blob = await (await fetch(src)).blob();
      toast((await copyImage(blob)) ? 'Logo copied' : 'Copy failed');
    }}>
      Copy logo
    </button>
  );
}

Catatan: PNG adalah tipe paling aman di mana pun; aplikasi Android juga menerima JPEG dan WebP.

appmintNative.Device appmintNative#

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

Contoh

Bentuk @capacitor/device: model ponsel, versi OS, ID yang stabil, dan bahasa. Memakai plugin Device Capacitor di aplikasi dan jawaban browser di web.

Mengembalikan: getInfo() → Promise<DeviceInfo & { operatingSystem, isVirtual }> (dengan Capacitor juga manufacturer, webViewVersion, …); getId() → Promise<{ identifier }>; getLanguageCode() → Promise<{ value }> (dua huruf, mis. "en"). Membutuhkan: tidak ada.

Tampilkan model dan versi OS di layar Tentang.

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

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

ID yang stabil dan kode bahasa.

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

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

Tampilkan versi aplikasi dan model perangkat di footer pengaturan. Laporan sinkronisasi memberikan versi aplikasi; plugin menambahkan sistem operasi.

import { deviceInfo, Device } from '@/lib/appmintNative';

export async function footerText(): Promise<string> {
  const base = deviceInfo();                            // sync: platform, language, model, appVersion
  const rich = await Device.getInfo();                  // async: adds operatingSystem, isVirtual
  const app = base.appVersion ? `v${base.appVersion}` : 'web';
  return `${app} · ${rich.model ?? 'browser'} · ${rich.operatingSystem}`;
}

Catatan: Di aplikasi, deviceInfo() sinkronisasi juga mengisi model dan osVersion; Device.getInfo() menambahkan operatingSystem, isVirtual dan kolom tambahan plugin. Di web operatingSystem adalah 'unknown' dan model kosong. getId() di web adalah ID localStorage dari installId() dan melempar error jika penyimpanan diblokir.

appmintNative.deviceInfo appmintNative#

deviceInfo(): DeviceInfo

Contoh

Memberi tahu secara sinkron di mana aplikasi berjalan (platform) dan bahasanya, ditambah laporan perangkat lengkap dari shell Android di dalam aplikasi Android.

Mengembalikan: DeviceInfo langsung: selalu { platform, language } - platform bernilai 'android' di aplikasi dan 'web' di browser. Di aplikasi Android ini juga mengisi model, manufacturer, osVersion (mis. "14") dan appVersion (mis. "1.4.0"), dan membawa setiap kunci dari WebToApk.getDeviceInfo(): android, hardware, screen, app, runtime, installId. Di web ini menambahkan osVersion (user agent). Dibutuhkan: tidak ada.

Cabangkan berdasarkan platform saat render.

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

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

Baca detail Android. Kolom umum ada di atas; sisa laporan shell tetap berada di grup bersarangnya.

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

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

Pilih kontrol yang tepat berdasarkan platform.

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

export function ShareHint() {
  const { platform, language } = deviceInfo();
  return platform === 'android'
    ? <p>Tap Share to send this from your phone.</p>
    : <p>Copy the link to share it ({language}).</p>;
}

Catatan: model dan appVersion hanya terisi di aplikasi. Di web, osVersion adalah string user-agent browser, bukan versi OS. Ini melempar kesalahan jika jawaban shell bukan JSON yang valid (cacat sebenarnya, tidak pernah diharapkan).

appmintNative.Dialog appmintNative#

Dialog.alert / Dialog.confirm / Dialog.prompt

Contoh

Dialog sistem asli: pesan, pertanyaan ya/tidak, pertanyaan teks satu baris. Aplikasi memakai plugin Dialog Capacitor; web memakai milik browser sendiri alert / confirm / prompt.

Mengembalikan: alert({ title?, message }) → Promise<void>. confirm({ title?, message, okButtonTitle?, cancelButtonTitle? }) → Promise<{ value: boolean }>. prompt({ title?, message, inputPlaceholder?, inputText? }) → Promise<{ value: string; cancelled: boolean }>. confirm dan prompt tidak pernah menolak (penutupan adalah sebuah jawaban); alert menolak hanya jika tidak bisa ditampilkan (tanpa pesan). Membutuhkan: tidak ada.

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

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

Alert dan prompt:

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

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

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

Konfirmasi sebelum keluar. Menutup dialog dianggap sebagai "tidak".

import { Dialog, toast } from '@/lib/appmintNative';

export async function signOut(doSignOut: () => Promise<void>) {
  const { value } = await Dialog.confirm({
    title: 'Sign out?',
    message: 'You will need to sign in again.',
    okButtonTitle: 'Sign out',
    cancelButtonTitle: 'Stay',
  });
  if (!value) return;
  await doSignOut();
  toast('Signed out');
}

Catatan: Menutup konfirmasi menghasilkan { value: false }; menutup prompt menghasilkan { value: '', cancelled: true }. Dialog memblokir layar - gunakan untuk keputusan yang benar-benar penting, dan toast() untuk pesan seperti "Tersimpan". prompt tidak meneruskan judul tombol kustom ke plugin.

appmintNative.dismissPushMessage appmintNative#

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

Contoh

Menyembunyikan satu pesan dari kotak masuk AppMint Push (geser untuk membuang). Urungkan dengan restorePushMessage(id).

Mengembalikan: boolean secara sinkron - true jika sebelumnya tersembunyi. Selalu false di web. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi.

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

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

Tampilkan kotak masuk dengan penangan geser dan status urungkan, sehingga geseran tidak pernah final.

import { useState } from 'react';
import { pushInbox, dismissPushMessage, restorePushMessage } from '@/lib/appmintNative';

export function Inbox() {
  const [items, setItems] = useState(pushInbox());
  const [undoId, setUndoId] = useState<string | null>(null);

  const swipe = (id: string) => {
    if (!dismissPushMessage(id)) return;     // false on the web or when AppMint Push is off
    setItems(pushInbox());
    setUndoId(id);
  };
  const undo = () => {
    if (undoId && restorePushMessage(undoId)) setItems(pushInbox());
    setUndoId(null);
  };

  return (
    <ul>
      {items.map((m) => <li key={m.id} onClick={() => swipe(m.id)}>{m.title}</li>)}
      {undoId && <button onClick={undo}>Undo</button>}
    </ul>
  );
}

Sembunyikan semua pesan yang sudah dibaca sekaligus, dan laporkan berapa banyak yang disembunyikan.

import { pushInbox, dismissPushMessage, toast } from '@/lib/appmintNative';

export function tidyReadMessages(): number {
  const hidden = pushInbox().filter((m) => m.read && dismissPushMessage(m.id)).length;
  toast(hidden ? `${hidden} message(s) hidden` : 'Nothing to hide');
  return hidden;
}

Catatan: hanya kotak masuk di ponsel ini yang berubah; notifikasi di panel dan riwayat kiriman Anda tetap ada.

appmintNative.docScanner appmintNative#

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

Contoh

Membuka pemindai dokumen sistem - deteksi tepi, perataan, beberapa halaman - pemindai ML Kit milik Google di aplikasi Android. Halaman dikembalikan sebagai URL data JPEG.

Mengembalikan: Promise<{ pages: string[]; pdfPath?: string } | null> - null saat pengguna membatalkan, pemindai tidak bisa dimulai, atau di web. isSupported() → Promise<boolean>: apakah ponsel ini bisa memindai (layanan Google Play); false di web. Membutuhkan: tidak ada yang perlu diaktifkan (di Android pemindai berjalan di layanan Google Play dan tidak butuh izin kamera).

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

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

Tanyakan dulu ke ponsel, karena perangkat lama mungkin tidak punya pemindai, lalu simpan halamannya.

import { useEffect, useState } from 'react';
import { docScanner, can } from '@/lib/appmintNative';

export function ScanPanel() {
  const [ready, setReady] = useState(false);
  const [pages, setPages] = useState<string[]>([]);

  useEffect(() => {
    if (can('docScanner') === 'none') return;          // web: no scanner at all
    docScanner.isSupported().then(setReady);           // false without Google Play services
  }, []);

  if (!ready) return <p>Scanning is not available on this phone.</p>;
  return (
    <>
      <button onClick={async () => {
        const res = await docScanner.scan({ pageLimit: 5 });
        if (!res) return;                              // cancelled or could not start
        setPages((p) => [...p, ...res.pages]);
      }}>Scan pages</button>
      {pages.map((src, i) => <img key={i} src={src} alt={`Page ${i + 1}`} width={120} />)}
    </>
  );
}

Pindai, lalu unggah halaman pertama dengan helper transfer file.

import { docScanner, fileTransfer } from '@/lib/appmintNative';

export async function scanAndUpload(): Promise<boolean> {
  const res = await docScanner.scan({ pageLimit: 1, quality: 0.7 });
  if (!res || res.pages.length === 0) return false;
  const blob = await (await fetch(res.pages[0])).blob();
  const file = new File([blob], 'page-1.jpg', { type: 'image/jpeg' });
  const r = await fileTransfer.upload('https://example.com/api/scans', file, { fieldName: 'scan' });
  return r.responseCode >= 200 && r.responseCode < 300;
}

Catatan: pageLimit 0 (bawaan) = tanpa batas; quality 0.1-1 (bawaan 0.85). Dengan pdf: true, pdfPath adalah path file di cache aplikasi, bukan URL yang bisa dimuat halaman. Data URL berukuran besar - ubah ukurannya atau unggah alih-alih menyimpan banyak di localStorage. Untuk foto biasa gunakan camera.take().

appmintNative.enableNotifications appmintNative#

enableNotifications(): Promise<EnableNotificationsResult>

Contoh

Membawa kembali pengguna yang menolak atau melewati notifikasi, dari tombol yang mereka ketuk: dialog izin selama masih bisa ditampilkan, jika tidak (aplikasi Android) pengaturan notifikasi aplikasi ini.

Mengembalikan: Promise<{ state, changed, openedSettings }> - state adalah 'granted' | 'prompt' | 'blocked' | 'unavailable', changed bernilai true jika berbeda dari sebelum panggilan, openedSettings bernilai true jika layar pengaturan dipakai. Tidak pernah ditolak. Di web meminta lewat Notification.requestPermission(). Membutuhkan: notifikasi di build (aktif setiap kali aplikasi mengirim atau menerima notifikasi).

import { enableNotifications, getNotificationState } from '@/lib/appmintNative';

export async function TurnOnNotificationsBanner(el: HTMLElement) {
  const state = await getNotificationState();
  el.hidden = !(state === 'prompt' || state === 'blocked');
  el.querySelector('button')!.addEventListener('click', async () => {
    const r = await enableNotifications();
    el.hidden = r.state === 'granted';
  });
}

Catatan: tampilkan banner hanya untuk 'prompt' dan 'blocked'; sembunyikan untuk 'granted' dan 'unavailable'. Tanyakan dari sebuah ketukan, jangan pernah saat peluncuran.

appmintNative.fileTransfer appmintNative#

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

Contoh

Mengunduh atau mengunggah file dengan callback progres. Di aplikasi, transfer berjalan secara native (Capacitor FileTransfer); di web, memakai fetch / XMLHttpRequest dengan callback yang sama.

Mengembalikan: download(url, fileName, opts?) → Promise<{ path?: string; blob?: Blob }> - path (file di cache aplikasi) di dalam aplikasi, blob di web (file juga disimpan melalui browser). upload(url, file, opts?) → Promise<{ responseCode: number; responseBody: string }>. upload selesai berapa pun status HTTP yang dijawab server (periksa responseCode), di aplikasi maupun web; keduanya ditolak jika transfernya sendiri gagal (tidak ada koneksi, file rusak). Dibutuhkan: tidak ada yang perlu diaktifkan.

Unduh dengan bilah kemajuan:

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

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

Unggah File dari sebuah <input type="file">:

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

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

Unduh dengan status progres, lalu buka file, dalam satu hook.

import { useState } from 'react';
import { fileTransfer, fileViewer } from '@/lib/appmintNative';

export function useOpenDocument() {
  const [percent, setPercent] = useState(0);
  const [error, setError] = useState<string | null>(null);

  const run = async (url: string, name: string) => {
    setError(null);
    try {
      const r = await fileTransfer.download(url, name, { onProgress: (p) => setPercent(Math.round(p.fraction * 100)) });
      if (r.path) await fileViewer.open(r.path);        // in the app; on the web the browser saved it
    } catch (e) {
      setError(String(e));
    }
  };
  return { percent, error, run };
}

Catatan: fraction bernilai 0 selama server tidak mengirim panjang. Di aplikasi, unduhan masuk ke folder CACHE aplikasi (bukan Downloads pengguna); untuk memberi pengguna salinannya, gunakan saveFile. Untuk unggahan, aplikasi lebih dulu menyalin file ke cache-nya, jadi file yang sangat besar memerlukan ruang kosong sebanyak itu. method (default 'POST') dan fileName adalah opsi unggahan yang tidak wajib.

appmintNative.fileViewer appmintNative#

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

Contoh

Membuka dokumen (PDF, gambar, file office) di aplikasi penampil ponsel. Di web, membuka alamatnya di tab baru.

Mengembalikan: Promise<boolean> - true jika penampil terbuka, false jika gagal (tidak ada aplikasi untuk tipe itu, path salah) atau target tidak bisa dibuka di permukaan ini. Dibutuhkan: tidak ada yang perlu diaktifkan; ini memakai plugin Capacitor FileViewer yang sudah ada di aplikasi.

Buka dokumen dari web:

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

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

Buka file yang sudah Anda unduh lebih dulu dengan fileTransfer.download (jalur lokal di aplikasi):

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

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

Buka tautan dari tombol dan beri tahu pengguna jika tidak ada penampil yang bisa membukanya.

import { fileViewer, toast } from '@/lib/appmintNative';

export function ManualButton({ url }: { url: string }) {
  return (
    <button onClick={async () => {
      if (!(await fileViewer.open(url))) toast('No app can open this file');
    }}>
      View manual
    </button>
  );
}

Catatan: Target http(s) memakai openDocumentFromUrl; yang lain diperlakukan sebagai jalur lokal (file:// dihapus). Di web hanya target http(s):, blob:, dan data: yang terbuka; jalur lokal menjawab false.

appmintNative.folder appmintNative#

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

Contoh

Seluruh folder yang diberi izin pengguna sekali dan dipertahankan aplikasi di setiap peluncuran, dengan operasi file sungguhan di dalamnya (di aplikasi: bridge folder milik shell).

Mengembalikan: setiap anggota bersifat async - connected() → boolean, label() → string, request() → boolean (false saat dibatalkan), disconnect() → void, list(path) → [{ name, uri, isDirectory, length, lastModified }], readText(path) → string | null (null = file tidak ada), writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean, stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. Membutuhkan: aplikasi buatan AI mengaktifkan sakelar Native Folder Access (SAF) sendiri saat kode memakai folder; pada build manual, centang di Langkah 4 (Akses).

Hubungkan, tampilkan folder, dan putuskan:

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

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

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

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

Baca, tulis, daftarkan, ganti nama, dan hapus - setiap path relatif terhadap folder yang diberi izin ('' adalah root-nya):

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

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

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

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

Cadangkan catatan ke folder yang diberi akses, dan beri tahu dengan jelas jika tidak ada folder yang terhubung.

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

export async function backupNote(name: string, text: string) {
  if (can('folderAccess') === 'none') return;
  if (!(await folder.connected()) && !(await folder.request())) {
    toast('Choose a folder to save backups');
    return;
  }
  const ok = await folder.writeText(`backups/${name}.md`, text);
  toast(ok ? 'Backed up' : 'Could not write to the folder');
}

Catatan: can('folderAccess') bernilai 'none' di web - tidak ada padanan folder permanen di browser ponsel, jadi jangan tampilkan fitur ini di sana (gunakan pickFile() / saveFile() untuk satu file). Panggil request() hanya dari ketukan pengguna. .. dan path absolut ditolak. Folder yang diberi izin itu sendiri tidak pernah bisa dihapus. stat() berfungsi untuk file dan subfolder (folder punya isDirectory: true dan size: 0); stat('') - root - adalah null, gunakan label() untuk itu. request() bisa dipanggil lagi setelah dibatalkan.

appmintNative.fullscreen appmintNative#

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

Contoh

Menyembunyikan bar sistem (di aplikasi) atau masuk ke layar penuh browser (di web), untuk video, game, atau tampilan membaca.

Mengembalikan: enter() dan exit() mengembalikan Promise<void> dan tidak pernah ditolak; isOn() mengembalikan boolean secara sinkron. Dibutuhkan: tidak ada.

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

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

Keluar dari layar penuh saat tampilan ditutup:

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

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

Sembunyikan bilah sistem untuk tampilan membaca dan ikuti statusnya dari sebuah tombol.

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

export function useReaderMode() {
  const [on, setOn] = useState(false);
  useEffect(() => () => { void fullscreen.exit(); }, []);        // always leave on unmount
  const toggle = async () => {
    if (can('fullscreen') === 'none') return;
    if (fullscreen.isOn()) await fullscreen.exit(); else await fullscreen.enter();
    setOn(fullscreen.isOn());                                   // a refused web request leaves it off
  };
  return { on, toggle };
}

Catatan:

  • Di aplikasi ini adalah layar penuh milik shell (bar status dan navigasi tersembunyi); selalu tersedia untuk halaman.
  • Di web, Fullscreen API hanya berfungsi dari ketukan pengguna. Permintaan yang ditolak diabaikan begitu saja, jadi baca isOn() sesudahnya alih-alih berasumsi.

appmintNative.geofences appmintNative#

import { geofences } from '@/lib/appmintNative'

Contoh

Geofence yang aktif saat aplikasi ditutup dan bertahan setelah restart: add, remove, list, events, clearEvents, onEvent, available.

Mengembalikan: add(spec) → Promise<BgResult> selesai setelah layanan Play menerima atau menolaknya ({ id, ok, reason? } - background_permission_required, location_off, too_many_geofences, …). remove / clearEvents → Promise<boolean>. list() → Promise<GeofenceSpec[]>. events() → Promise<GeofenceEvent[]> (100 terakhir, termasuk yang terjadi saat aplikasi tertutup). onEvent(cb) → fungsi berhenti berlangganan. Membutuhkan: Geofences pada build dan "Izinkan sepanjang waktu" (backgroundLocation.requestAlways()).

import { geofences, backgroundLocation } from '@/lib/appmintNative';

export async function remindAtStore(lat: number, lng: number) {
  let r = await geofences.add({ id: 'store', lat, lng, radiusM: 200, events: ['enter'],
    notify: { title: 'You are near the store', body: 'Your list is ready' } });
  if (!r.ok && r.reason === 'background_permission_required') {
    const p = await backgroundLocation.requestAlways();
    if (p.background === 'granted') r = await geofences.add({ id: 'store', lat, lng, radiusM: 200, events: ['enter'] });
  }
  return r.ok;
}

geofences.onEvent((e) => console.log(e.id, e.event, new Date(e.time)));

Tampilkan daftar geofence aktif dan transisi yang tercatat saat aplikasi tertutup.

import { geofences } from '@/lib/appmintNative';

export async function geofenceReport(): Promise<string[]> {
  if (geofences.available() === 'none') return [];      // web build: no geofences
  const active = await geofences.list();
  const events = await geofences.events();              // oldest first, last 100
  return [
    `${active.length} active zone(s)`,
    ...events.map((e) => `${e.id} ${e.event} at ${new Date(e.time).toLocaleTimeString()}`),
  ];
}

Tambahkan zona kerja untuk masuk dan keluar, tanggapi penolakan berdasarkan alasannya, dan hapus kemudian.

import { geofences } from '@/lib/appmintNative';

export async function watchOffice(lat: number, lng: number): Promise<string> {
  const r = await geofences.add({ id: 'office', lat, lng, radiusM: 150, events: ['enter', 'exit'] });
  if (r.ok) return 'Office zone is on';
  if (r.reason === 'location_off') return 'Turn location on to use this';
  if (r.reason === 'too_many_geofences') return 'Remove a zone first';
  return `Could not add the zone (${r.reason ?? 'unknown'})`;
}

export async function stopWatchingOffice() {
  await geofences.remove('office');
  await geofences.clearEvents();
}

appmintNative.Geolocation appmintNative#

Geolocation.getCurrentPosition / watchPosition / clearWatch / checkPermissions / requestPermissions

Contoh

Bentuk @capacitor/geolocation: plugin Capacitor Geolocation di aplikasi, navigator.geolocation standar di web. Semuanya mengembalikan objek posisi W3C.

Mengembalikan: getCurrentPosition(opts) → Promise<GeolocationPosition> (ditolak jika pengguna menolak, waktu habis, atau tidak ada lokasi); watchPosition(opts, cb) → Promise<string> (id pemantauan); clearWatch({ id }) → Promise<void>; checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (masing-masing 'granted' | 'denied' | 'prompt'; di Android 12+ pengguna yang memilih "Approximate" hanya memberikan coarseLocation). Dibutuhkan: GPS / Location di Langkah 4 (Access) - build mengaktifkan keduanya saat kode Anda memanggil getCurrentPosition atau watchPosition.

Saya di mana, sekali saja.

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

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

Lacak perjalanan, lalu berhenti. Callback mendapat null dan kesalahan saat pembacaan lokasi gagal.

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

let watchId: string | null = null;

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

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

Ambil posisi sekali. Mengembalikan null jika pengguna menolak.

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

export async function nearestCoords(): Promise<{ lat: number; lng: number } | null> {
  try {
    const pos = await Geolocation.getCurrentPosition({ timeout: 10000 });
    return { lat: pos.coords.latitude, lng: pos.coords.longitude };
  } catch {
    return null;                          // refused, timed out or location is off
  }
}

Catatan: navigator.geolocation biasa juga berfungsi di aplikasi; gunakan ini jika Anda ingin bentuk Capacitor. Di Android 12+ pengguna mungkin hanya mengizinkan lokasi perkiraan - Anda tetap mendapat posisi, hanya kurang akurat. Di web requestPermissions() membuat satu permintaan posisi sungguhan untuk memicu prompt browser. Lokasi adalah data pribadi: deklarasikan di formulir Data safety Play Anda.

appmintNative.getNotificationState appmintNative#

getNotificationState(): Promise<NotificationState>

Contoh

Apakah notifikasi menyala, dan jika tidak, apakah aplikasi masih bisa meminta atau hanya Pengaturan yang bisa memperbaikinya.

Mengembalikan: Promise<'granted' | 'prompt' | 'blocked' | 'unavailable'>. Di web, Notification.permission dipetakan dengan cara yang sama (default → 'prompt', denied → 'blocked'); 'unavailable' jika sama sekali tidak ada notifikasi. Membutuhkan: tidak ada.

import { getNotificationState } from '@/lib/appmintNative';

export async function shouldShowNotifBanner(): Promise<boolean> {
  const state = await getNotificationState();
  return state === 'prompt' || state === 'blocked';
}

Catatan: periksa lagi saat aplikasi kembali ke latar depan (visibilitychange) - pengguna mungkin mengubahnya di Pengaturan.

appmintNative.getPushTags appmintNative#

import { getPushTags } from '@/lib/appmintNative'

Contoh

Membaca tag AppMint Push instalasi ini sebagai objek.

Mengembalikan: Record<string, string> secara sinkron - mis. { plan: 'pro', cart: 'open' }; {} jika tidak ada dan di web. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrasi (Langkah 3) saat Anda membuat build.

import { getPushTags, setPushTag } from '@/lib/appmintNative';

// Every setPushTag makes the install check in, so skip the calls that change nothing.
export function ensurePushTag(key: string, value: string) {
  if ((getPushTags()[key] ?? '') !== value) setPushTag(key, value);
}

ensurePushTag('plan', 'pro');

Atur beberapa tag sekaligus dengan setPushTags saat satu layar mengubah lebih dari satu, dan kosongkan nilai untuk menghapus tag-nya:

import { setPushTags } from '@/lib/appmintNative';

// Checkout finished: the cart tag goes away and the plan tag is set, all saved or none.
export function onCheckoutDone(plan: string) {
  const saved = setPushTags({ cart: '', plan });
  if (!saved) console.warn('push tags not saved (web, AppMint Push off, or a refused key or value)');
}

Baca kembali tag untuk menampilkan petunjuk hanya jika instalasi ini punya tag:

import { getPushTags } from '@/lib/appmintNative';

export function OffersHint() {
  const tags = getPushTags();                       // {} on the web
  if (Object.keys(tags).length === 0) return null;
  return <p>Offers for the {tags.plan ?? 'standard'} plan are on.</p>;
}

Catatan: ini adalah yang disimpan ponsel; pusat pengiriman melihatnya setelah check-in berikutnya dari instalasi.

appmintNative.getPushUser appmintNative#

import { getPushUser } from '@/lib/appmintNative'

Contoh

Membaca ID pengguna yang terakhir diberikan instalasi ini ke AppMint Push dengan setPushUser.

Mengembalikan: string secara sinkron - id tersimpan, atau '' jika belum masuk, belum pernah diatur, atau di web. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi.

import { getPushUser, setPushUser } from '@/lib/appmintNative';

// On app start: the session expired while the app was closed, so stop personal messages here.
export function syncPushUser(currentUserId: string | null) {
  const want = currentUserId ?? '';
  if (getPushUser() !== want) setPushUser(want);
}

Panggil setPushUser dengan ID akun setelah masuk dan dengan '' saat keluar; mengembalikan false jika ID ditolak atau AppMint Push nonaktif:

import { setPushUser } from '@/lib/appmintNative';

export function onSignedIn(userId: string) {
  if (!setPushUser(userId)) console.warn('push user id not saved');   // ids only: no email or phone number
}

export function onSignedOut() {
  setPushUser('');
}

Baca id yang tersimpan di hook React, bersama tag milik pengguna tersebut:

import { useEffect, useState } from 'react';
import { getPushUser, setPushTag } from '@/lib/appmintNative';

export function usePushUser(plan: string) {
  const [id] = useState(getPushUser);
  useEffect(() => {
    if (id) setPushTag('plan', plan);     // only tag installs that belong to a signed-in user
  }, [id, plan]);
  return id;
}

Catatan: ini adalah apa yang dimiliki ponsel; pusat pengiriman melihat perubahan setelah pemeriksaan berikutnya dari pemasangan tersebut, beberapa detik kemudian saat online.

appmintNative.getVolume appmintNative#

getVolume(stream) — @/lib/appmintNative

Contoh

Membaca level volume ponsel untuk satu stream: 'media', 'alarm', 'ring', 'notification' atau 'call'.

Mengembalikan: { stream, current, max, percent, muted? } (current/max dalam tingkat bawaan ponsel, percent 0-100), atau null di web. Dibutuhkan: tidak ada.

import { getVolume } from '@/lib/appmintNative';

export function AlarmVolumeHint() {
  const v = getVolume('alarm');
  if (!v || v.percent > 0) return null;
  return <p role="alert">Your alarm volume is off, so the alarm will not be heard.</p>;
}

getVolume menjawab null di web, jadi gambar levelnya hanya jika berupa objek:

import { getVolume } from '@/lib/appmintNative';

export function MediaLevel() {
  const v = getVolume('media');
  if (!v) return null;                       // web: no phone volume to show
  return <progress max={100} value={v.percent} aria-label="Media volume" />;
}

Ikuti tombol fisik dengan onVolumeChange, dan naikkan level yang pelan dengan setVolume:

import { useEffect, useState } from 'react';
import { getVolume, onVolumeChange, setVolume, requestDndAccess } from '@/lib/appmintNative';

export function useMediaPercent(): number | null {
  const [pct, setPct] = useState(() => getVolume('media')?.percent ?? null);
  useEffect(() => onVolumeChange((l) => { if (l.stream === 'media') setPct(l.percent); }), []);
  return pct;
}

export function raiseIfQuiet() {
  const v = getVolume('media');
  if (!v || v.percent >= 40) return;
  const r = setVolume('media', 60, true);
  if (!r.ok && r.error === 'dnd_access_required') requestDndAccess();   // the user must allow Do Not Disturb access
}

appmintNative.haptic appmintNative#

haptic(kind?) — @/lib/appmintNative

Contoh

Getaran singkat yang terasa asli untuk ketukan dan hasil. Panggil pada setiap ketukan yang berarti.

Mengembalikan: void (kirim lalu lupakan). kind: 'light' (default), 'medium', 'heavy', 'selection', 'success', 'warning', 'error'. Dibutuhkan: Vibrate di Langkah 4 (Access); build AI mengaktifkannya untuk Anda saat kode memanggil haptic.

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

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

Untuk tab, sakelar, dan pemilih gunakan 'selection'; untuk konfirmasi hapus gunakan 'heavy':

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

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

Tombol suka yang bergetar ringan saat menyala dan lebih kuat saat mati:

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

export function LikeButton({ liked, onToggle }: { liked: boolean; onToggle: () => void }) {
  return (
    <button aria-pressed={liked} onClick={() => { haptic(liked ? 'light' : 'medium'); onToggle(); }}>
      {liked ? 'Liked' : 'Like'}
    </button>
  );
}

Catatan: Di aplikasi ini memakai plugin Haptics Capacitor (impact, notification, selectionChanged). Di web memakai navigator.vibrate jika browser memilikinya, dan tidak melakukan apa-apa di tempat lain; tidak pernah melempar kesalahan. Untuk ritme yang lebih panjang gunakan hapticPlay.

appmintNative.hapticPlay appmintNative#

hapticPlay(pattern) — @/lib/appmintNative

Contoh

Memainkan pola getaran yang kaya untuk momen khusus (kemenangan, rentetan, detak jantung). Gunakan haptic() untuk ketukan biasa.

Mengembalikan: Promise<boolean>: true jika pola berhasil dimainkan, false jika tidak bisa. Dibutuhkan: Vibrate di Langkah 4 (Access).

Pola bernama: 'success-ramp', 'heartbeat', 'tick-tick-thud', 'drum-roll', 'nudge'.

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

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

Pola Anda sendiri: daftar { at, duration?, intensity?, sharpness? } (ms, ms, 0-1, 0-1):

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

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

Gabungkan dengan haptic(): umpan balik ketukan normal untuk setiap jawaban, pola panjang hanya untuk momen istimewa:

import { haptic, hapticPlay } from '@/lib/appmintNative';

export async function onAnswer(correct: boolean, streak: number) {
  haptic(correct ? 'success' : 'error');            // every answer
  if (correct && streak > 0 && streak % 5 === 0) {
    await hapticPlay('drum-roll');                  // every fifth in a row
  }
}

Catatan: Aplikasi memakai plugin Capacitor AppwrightHaptics (kekuatan per ketukan jika ponsel mendukung). Web hanya mendapat pengaturan waktunya. sharpness diterima tetapi tidak berpengaruh di Android. haptic.play(pattern) adalah fungsi yang sama.

appmintNative.Haptics appmintNative#

Haptics — @/lib/appmintNative

Contoh

Objek Haptics bergaya Capacitor: nama dan opsi yang sama dengan @capacitor/haptics, berjalan di kode yang sama dengan haptic().

Mengembalikan: setiap metode mengembalikan void (bukan Promise; await padanya tidak berbahaya). Membutuhkan: Vibrate di Langkah 4 (Akses); build AI mengaktifkannya untuk Anda saat kode memakai Haptics.

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

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

Pemilih yang berdetak saat nilainya berubah:

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

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

Formulir yang mengonfirmasi dengan getaran notifikasi dan menambah getaran singkat saat ada kolom yang salah:

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

export function onSubmit(valid: boolean) {
  Haptics.notification({ type: valid ? 'SUCCESS' : 'ERROR' });
  if (!valid) Haptics.vibrate({ duration: 200 });
}

Catatan: selectionStart dan selectionChanged sama-sama memberi satu getaran pemilihan; selectionEnd tidak melakukan apa-apa. Berfungsi di aplikasi (plugin Haptics dari Capacitor); di web memakai navigator.vibrate jika tersedia, jika tidak tidak melakukan apa-apa.

appmintNative.has appmintNative#

has(cap: Capability): boolean

Contoh

Bentuk ya/tidak dari can(): true jika fitur akan benar-benar berfungsi di permukaan ini.

Mengembalikan: boolean, secara sinkron - tepatnya can(cap) !== 'none'. Membutuhkan: tidak ada.

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

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

Batasi seluruh fitur:

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

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

Pilih label berdasarkan tempat berjalannya: has() menentukan apakah kontrol digambar, can() memberi tahu jenisnya:

import { can, has } from '@/lib/appmintNative';

export function shareLabel(): string | null {
  if (!has('share')) return null;                      // nothing to share with: draw no button
  return can('share') === 'native' ? 'Share…' : 'Share link';
}

Catatan: Ini menerima nama kemampuan yang sama seperti can() (lihat entri itu untuk daftar lengkapnya). Gunakan can() saat Anda perlu tahu DI MANA ia berjalan ('native' atau 'web'), misalnya untuk mengubah label.

appmintNative.hideKeyboard appmintNative#

hideKeyboard(): void

Contoh

Menutup keyboard di layar - misalnya saat formulir dikirim atau sheet terbuka di atas kolom isian.

Mengembalikan: tidak ada. Membutuhkan: tidak ada.

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

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

Tutup keyboard saat lembar terbuka di atas kolom, agar lembar tidak tersembunyi di belakangnya:

import { useState } from 'react';
import { hideKeyboard } from '@/lib/appmintNative';

export function useSheet() {
  const [open, setOpen] = useState(false);
  return {
    open,
    show: () => { hideKeyboard(); setOpen(true); },
    hide: () => setOpen(false),
  };
}

Tutup saat pengguna mengetuk di mana pun yang bukan kolom teks:

import type { ReactNode } from 'react';
import { hideKeyboard } from '@/lib/appmintNative';

export function Screen({ children }: { children: ReactNode }) {
  return (
    <div onPointerDown={(e) => {
      if (!(e.target as HTMLElement).closest('input, textarea')) hideKeyboard();
    }}>
      {children}
    </div>
  );
}

Catatan: Di aplikasi, ini meminta plugin Keyboard Capacitor menyembunyikan keyboard. Di web, ini melepas fokus dari kolom yang aktif, yang menutup keyboard di browser. Aman dipanggil saat tidak ada keyboard yang terbuka.

appmintNative.inAppCampaigns appmintNative#

import { inAppCampaigns } from '@/lib/appmintNative'

Contoh

Pesan dalam aplikasi yang dipegang instalasi ini saat ini, sebagai {id, name}[]: kampanye kreator yang audiensnya mencakup pengguna ini.

Mengembalikan: { id: string; name: string }[] secara sinkron; [] di web dan tanpa AppMint Push. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat build.

import { inAppCampaigns } from '@/lib/appmintNative';

console.log(inAppCampaigns().map((c) => c.name));

Daftar debug di layar admin:

import { inAppCampaigns } from '@/lib/appmintNative';

export function InAppDebug() {
  const list = inAppCampaigns();
  return <ul>{list.length ? list.map((c) => <li key={c.id}>{c.name}</li>) : <li>None for this install</li>}</ul>;
}

Lewati petunjuk Anda sendiri jika pembuat sudah menjalankan pesan dengan nama itu:

import { inAppCampaigns } from '@/lib/appmintNative';

export const hasCampaign = (name: string) => inAppCampaigns().some((c) => c.name === name);

appmintNative.installId appmintNative#

installId(): string

Contoh

Memberikan ID acak yang stabil untuk instalasi ini: ID milik shell sendiri di aplikasi Android, ID acak yang disimpan di localStorage di web.

Mengembalikan: string seketika. Membutuhkan: tidak ada.

Kaitkan uji coba gratis dengan perangkat.

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

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

Hitung instalasi ini satu kali di analitik Anda sendiri.

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

export function reportFirstOpen() {
  let id: string;
  try { id = installId(); } catch { return; }          // web with storage blocked: no stable id
  if (localStorage.getItem('first-open-sent')) return;
  navigator.sendBeacon('https://api.example.com/first-open', JSON.stringify({ install: id }));
  localStorage.setItem('first-open-sent', '1');
}

Tampilkan kode dukungan singkat yang bisa Anda cari nanti, dengan tombol salin.

import { installId, copy, toast } from '@/lib/appmintNative';

export function SupportCode() {
  let code = '';
  try { code = installId().slice(0, 8); } catch { /* storage blocked: show nothing */ }
  if (!code) return null;
  return <button onClick={async () => { if (await copy(code)) toast('Copied'); }}>Support code {code}</button>;
}

Catatan: Nilainya tetap sama setelah pembaruan aplikasi dan berubah saat dicopot atau "hapus data". Ini bukan ID perangkat keras. Di web ini melempar error jika localStorage diblokir, dengan sengaja - ID kosong yang dipakai bersama akan mencampuradukkan pengguna.

appmintNative.isAlarmRinging appmintNative#

isAlarmRinging() — @/lib/appmintNative

Contoh

Apakah salah satu alarm aplikasi sedang berbunyi sekarang.

Mengembalikan: boolean, secara sinkron; selalu false di web. Dibutuhkan: 🔔 Alarm clock di langkah Akses (Langkah 4) saat Anda membuat aplikasi.

import { useEffect, useState } from 'react';
import { isAlarmRinging, onAlarm } from '@/lib/appmintNative';

export function useRinging(): boolean {
  const [ringing, setRinging] = useState(isAlarmRinging);
  useEffect(() => onAlarm(() => setRinging(isAlarmRinging())), []);
  return ringing;
}

Tampilkan tombol Berhenti dan Tunda saat alarm berbunyi:

import { isAlarmRinging, stopAlarm, snoozeAlarm } from '@/lib/appmintNative';

export function RingingBar() {
  if (!isAlarmRinging()) return null;               // always false on the web
  return (
    <div role="alert">
      <button onClick={() => stopAlarm()}>Stop</button>
      <button onClick={() => snoozeAlarm('', 5)}>Snooze 5 min</button>
    </div>
  );
}

Periksa dukungan lebih dulu, agar halaman hanya menampilkan kontrol alarm di tempat alarm bisa berbunyi:

import { alarmSupport, isAlarmRinging } from '@/lib/appmintNative';

export function alarmState(): 'hidden' | 'ringing' | 'idle' {
  if (alarmSupport() === 'unavailable') return 'hidden';   // web or iOS: no alarm controls
  return isAlarmRinging() ? 'ringing' : 'idle';
}

appmintNative.isNativeApp appmintNative#

isNativeApp(): boolean

Contoh

Memberi tahu apakah kode berjalan di dalam aplikasi Android yang terinstal (bridge shell window.WebToApk tersedia).

Mengembalikan: boolean seketika. false di browser. Membutuhkan: tidak ada.

Tampilkan badge "Aplikasi Android", dan tautan unduh di browser.

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

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

Beri tag pada event analitik Anda dengan asalnya.

import { isNativeApp, platform } from '@/lib/appmintNative';

export function analyticsSource(): string {
  return isNativeApp() ? `app-${platform()}` : 'browser';   // 'app-android' or 'browser'
}

Sembunyikan banner "Dapatkan aplikasinya" di dalam aplikasi.

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

export function InstallBanner() {
  if (isNativeApp()) return null;        // already installed: no banner
  return <a href="https://play.google.com/store/apps/details?id=com.example.app">Get the app</a>;
}

Catatan: platform() memberi jawaban yang sama sebagai string ('android' di aplikasi, 'web' di browser). Untuk memutuskan apakah suatu fitur bisa ditampilkan, gunakan can('<capability>') sebagai gantinya: ia tahu platform mana yang punya fitur apa.

appmintNative.keepAlive appmintNative#

import { keepAlive } from '@/lib/appmintNative'

Contoh

Menjaga aplikasi tetap berjalan di latar belakang dengan notifikasi foreground-service (start, update, stop, isRunning, onState, available).

Mengembalikan: start / update → Promise<BgResult> ({ ok, reason?, type?, notificationShown? }; reason: 'unsupported' di browser atau iOS, 'not_enabled' jika build tidak mencentang Keep running in background). stop → Promise<boolean>. isRunning() → boolean. onState(cb) → fungsi berhenti berlangganan; cb menerima detail appmint:background-service. Dibutuhkan: Keep running in background di build.

import { keepAlive } from '@/lib/appmintNative';

export async function startRecording() {
  if (keepAlive.available() === 'none') return;
  const r = await keepAlive.start({ title: 'Recording', text: 'Tap to return', stopLabel: 'Stop' });
  if (!r.ok) console.warn('keep running refused:', r.reason);
}

const off = keepAlive.onState((d) => {
  if (d.state === 'stopped') console.log('service stopped:', d.reason);
});
// later: await keepAlive.update({ title: 'Recording', text: '12:30' }); await keepAlive.stop(); off();

Hentikan layanan, dan baca mengapa start ditolak:

import { keepAlive } from '@/lib/appmintNative';

export async function toggleTracking(on: boolean) {
  if (!on) { await keepAlive.stop(); return; }
  const r = await keepAlive.start({ title: 'Tracking', text: 'Running', type: 'dataSync' });
  if (!r.ok) {
    // 'unsupported' = browser or iOS; 'not_enabled' = the build did not tick Keep running in background
    alert(r.reason === 'not_enabled' ? 'Turn on Keep running in background and rebuild.' : 'Not available here.');
  }
}

Selaraskan tombol React dengan layanan, dan tampilkan progres di notifikasi:

import { useEffect, useState } from 'react';
import { keepAlive } from '@/lib/appmintNative';

export function useKeepAlive() {
  const [running, setRunning] = useState(keepAlive.isRunning);
  useEffect(() => keepAlive.onState((d) => setRunning(d.state === 'running')), []);
  return running;
}

export function reportProgress(done: number, total: number) {
  void keepAlive.update({ title: 'Uploading', text: `${done} of ${total}`, progress: { value: done, max: total } });
}

appmintNative.keepAwake appmintNative#

keepAwake(on: boolean): Promise<boolean>

Contoh

Menjaga layar tetap menyala saat resep, rute, kode QR, atau timer sedang tampil, dan membiarkannya tidur lagi.

Mengembalikan: Promise<boolean> - true jika permintaan berlaku, false jika tidak ada yang bisa menjaga layar tetap menyala (atau browser menolak). Membutuhkan: tidak ada.

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

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

Jaga layar tetap menyala hanya saat timer berjalan, dan beri tahu pengguna jika gagal.

import { keepAwake, toast } from '@/lib/appmintNative';
import { useEffect } from 'react';

export function useAwakeWhile(running: boolean) {
  useEffect(() => {
    if (!running) return;
    let held = false;
    void keepAwake(true).then((ok) => {
      held = ok;
      if (!ok) toast('Your screen may turn off while the timer runs');
    });
    return () => { if (held) void keepAwake(false); };
  }, [running]);
}

Periksa kemampuannya lebih dulu dan ambil kunci lagi saat tab kembali ke depan (web melepasnya saat halaman tersembunyi).

import { keepAwake, can } from '@/lib/appmintNative';

export function holdScreenOn(): () => void {
  if (can('keepAwake') === 'none') return () => {};
  void keepAwake(true);
  const again = () => { if (document.visibilityState === 'visible') void keepAwake(true); };
  document.addEventListener('visibilitychange', again);
  return () => {
    document.removeEventListener('visibilitychange', again);
    void keepAwake(false);
  };
}

Catatan: Di aplikasi, ini memakai plugin KeepAwake (flag layar-tetap-menyala pada jendela); di web, Screen Wake Lock API, yang dilepas browser dengan sendirinya saat tab disembunyikan - panggil keepAwake(true) lagi saat halaman terlihat jika layar harus tetap menyala. Ini hanya berfungsi saat aplikasi berada di depan.

appmintNative.Keyboard appmintNative#

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

Contoh

Keyboard perangkat lunak dalam bentuk plugin Keyboard milik Capacitor: tutup, dan dengarkan saat ruang yang ditutupinya berubah.

Mengembalikan: hide() → Promise<void>. addListener(event, handler) → bentuk Capacitor: Promise dari handle yang juga punya remove() sendiri, jadi await, .then(h => h.remove()) dan .remove() semuanya berfungsi. event adalah 'keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide' atau 'keyboardDidHide'; handler menerima { keyboardHeight }. Dibutuhkan: tidak ada.

Tutup keyboard saat formulir dikirim atau sheet terbuka di atas kolom isian:

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

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

Dengarkan (lepaskan handle saat dilepas):

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

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

Gabungkan keduanya untuk menggulir kolom yang aktif ke dalam tampilan saat keyboard terbuka, lalu tutup keyboard saat Enter:

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

export function useKeyboardScroll() {
  useEffect(() => {
    const h = Keyboard.addListener('keyboardDidShow', () => {
      document.activeElement?.scrollIntoView({ block: 'center', behavior: 'smooth' });
    });
    const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter') void Keyboard.hide(); };
    window.addEventListener('keydown', onKey);
    return () => { h.remove(); window.removeEventListener('keydown', onKey); };
  }, []);
}

Catatan: keyboardHeight adalah bagian keyboard yang masih harus dihindari halaman, bukan tinggi mentah: di aplikasi Android shell mengubah ukuran halaman untuk keyboard, jadi nilainya 0 di sana saat keyboard terbuka (event show tetap aktif, dan aktif lagi saat angkanya berubah). Event hide hanya aktif saat keyboard benar-benar menutup, jadi bisa melacak "apakah keyboard terbuka". Event "Will" dan "did" aktif bersamaan. Untuk padding saja, onKeyboard((px) => …) lebih sederhana.

appmintNative.launchUrl appmintNative#

launchUrl(): Promise<string>

Contoh

Memberikan tautan yang meluncurkan atau melanjutkan aplikasi - deep link, pintasan ikon, ketukan widget, notifikasi - agar aplikasi bisa membuka layar yang tepat.

Mengembalikan: Promise<string> - URL lengkap, atau '' jika aplikasi dibuka secara normal. Selalu '' di web, tempat bilah alamat sudah menjadi rute. Dibutuhkan: tidak ada untuk pintasan dan ketukan widget; tautan dari luar memerlukan opsi Deep Linking saat Anda membuat aplikasi.

Arahkan berdasarkan itu sekali saat aplikasi dimuat:

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

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

Pasangkan dengan onDeepLink() untuk tautan yang masuk saat aplikasi sudah terbuka:

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

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

Perlakukan string kosong sebagai "dibuka secara normal" dan pilih layar awal dari tautan hanya jika ada. Pintasan seperti appmint-shortcut://<package>/orders memiliki rute di path-nya.

import { launchUrl, onDeepLink } from '@/lib/appmintNative';

export async function startRoute(): Promise<string> {
  const u = await launchUrl();
  if (!u) return '/';                         // normal start, or the web
  const { pathname, search } = new URL(u);
  return pathname + search;
}

// While the app stays open, later links arrive here.
export function watchLinks(go: (route: string) => void): () => void {
  return onDeepLink((u) => { const x = new URL(u); go(x.pathname + x.search); });
}

Catatan:

  • Nilainya DISIMPAN, tidak dihabiskan: komponen yang dipasang belakangan tetap melihatnya. Tautan yang lebih baru menggantikannya.
  • Ini dibaca dari shell (WebToApk.getLaunchUrl()); jangan pernah memanggilnya langsung di aplikasi buatan AI. Pintasan dan ketukan widget tiba sebagai appmint-shortcut://<package><route>.
  • Parse dengan new URL(u) dan navigasikan ke path-nya; jangan memuat URL itu sendiri.

appmintNative.listCallLog appmintNative#

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

Contoh

Membaca riwayat panggilan, terbaru lebih dulu. Hanya aplikasi Android.

Mengembalikan: Promise<CallLogEntry[]> - masing-masing { number, name, type, date, duration }; type adalah incoming, outgoing, missed, voicemail, rejected, blocked atau other, date adalah milidetik epoch, duration dalam detik. Kosong jika Call log nonaktif atau izin ditolak. Membutuhkan: sakelar Call log (read) di Langkah 4 (Akses) - build mengaktifkannya saat kode Anda memanggil ini - dan izin pengguna saat pertama kali dipakai.

Panggilan tak terjawab dalam 7 hari terakhir.

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

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

Total waktu bicara per kontak dari panggilan keluar dan masuk, terlama lebih dulu.

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

export async function talkTimeByPerson() {
  if (can('callLog') === 'none') return null;
  const calls = await listCallLog(1000, 0).catch(() => []);
  const totals = new Map<string, number>();
  for (const c of calls) {
    if (c.type !== 'incoming' && c.type !== 'outgoing') continue;
    const who = c.name || c.number || 'Unknown';
    totals.set(who, (totals.get(who) ?? 0) + (c.duration ?? 0));   // seconds
  }
  return [...totals].sort((a, b) => b[1] - a[1]);
}

Layar yang menjelaskan daftar kosong, karena "tidak ada panggilan" dan "izin ditolak" terlihat sama.

import { listCallLog, can } from '@/lib/appmintNative';
import { useEffect, useState } from 'react';

export function useCalls() {
  const [calls, setCalls] = useState<Awaited<ReturnType<typeof listCallLog>> | null>(null);
  const supported = can('callLog') !== 'none';
  useEffect(() => {
    if (supported) void listCallLog(50).then(setCalls).catch(() => setCalls([]));
  }, [supported]);
  const hint = !supported ? 'Open the app on your phone to see calls'
    : calls && calls.length === 0 ? 'No calls found. Allow call log access in Settings if you expected some.' : '';
  return { calls: calls ?? [], hint };
}

Catatan: limit dibatasi hingga 2000 oleh aplikasi. Daftar kosong tidak bisa membedakan "tidak ada panggilan" dari "izin ditolak". can('callLog') adalah 'none' di web. Log panggilan adalah izin terbatas Google Play: Anda butuh Permissions Declaration yang disetujui di Play Console, atau aplikasi ditolak.

appmintNative.listContacts appmintNative#

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

Contoh

Membaca buku kontak, satu halaman demi satu halaman. Hanya untuk aplikasi Android, melalui bridge shell.

Mengembalikan: Promise<Contact[]>. Array kosong jika tidak ada kontak, jika akses ditolak, atau jika Kontak tidak diaktifkan saat build. Membutuhkan: sakelar Kontak di Langkah 4 (Akses) - build mengaktifkannya saat kode Anda memanggil ini - dan izin pengguna saat pertama kali dipakai.

Layar daftar kontak. Tampilkan hanya di tempat yang bisa berfungsi. Setiap kontak adalah { id?, name?, phones?: string[], emails?: string[], photoUri? }.

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

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

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

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

Cari di seluruh buku kontak dengan membacanya halaman demi halaman ke dalam satu daftar.

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

export async function findContacts(query: string): Promise<Contact[] | null> {
  if (can('contactList') === 'none') return null;
  const all: Contact[] = [];
  for (let offset = 0; ; offset += 200) {
    const page = await listContacts(200, offset).catch(() => []);
    all.push(...page);
    if (page.length < 200) break;
  }
  const q = query.trim().toLowerCase();
  return all.filter((c) => (c.name ?? '').toLowerCase().includes(q));
}

Pasangkan dengan sendSms untuk mengirim pesan ke nomor pertama kontak.

import { sendSms, type Contact } from '@/lib/appmintNative';

export async function invite(c: Contact): Promise<boolean> {
  const phone = c.phones?.[0];
  if (!phone) return false;
  return sendSms(phone, `Hi ${c.name ?? ''}, join me on the app!`);
}

Catatan: Hasil kosong tidak bisa membedakan "tidak ada kontak" dari "izin ditolak" - tampilkan petunjuk singkat ("Izinkan kontak di Pengaturan") saat daftar kosong. Kontak diurutkan A sampai Z berdasarkan nama; limit dibatasi hingga 2000. Untuk satu orang gunakan pickContact(), yang tidak butuh izin. Kontak adalah data pribadi: nyatakan di formulir Data safety Play dan kebijakan privasi Anda.

appmintNative.listSms appmintNative#

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

Contoh

Membaca pesan SMS dari kotak masuk, kotak terkirim, atau draf, terbaru lebih dulu. Hanya aplikasi Android.

Mengembalikan: Promise<SmsMessage[]> - masing-masing { id, address, body, date, read, box }. Kosong jika SMS nonaktif, izin ditolak, atau kotaknya kosong. Membutuhkan: sakelar SMS (baca & kirim) di Langkah 4 (Akses) - build mengaktifkannya saat kode Anda memanggil ini - dan izin pengguna saat pertama kali dipakai.

Tampilkan 20 pesan terbaru dari kotak masuk.

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

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

Pesan belum dibaca dari satu pengirim, terbaru lebih dulu.

import { listSms, can } from '@/lib/appmintNative';

export async function unreadFrom(address: string) {
  if (can('smsList') === 'none') return null;
  const msgs = await listSms('inbox', 200, 0).catch(() => []);
  return msgs.filter((m) => m.address === address && !m.read);
}

Baca kotak terkirim dan draf sekaligus, dan urutkan bersama berdasarkan tanggal.

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

export async function outgoing(): Promise<SmsMessage[]> {
  const [sent, drafts] = await Promise.all([
    listSms('sent', 50, 0).catch(() => []),
    listSms('draft', 50, 0).catch(() => []),
  ]);
  return [...sent, ...drafts].sort((a, b) => (b.date ?? 0) - (a.date ?? 0));
}

Catatan: Daftar kosong tidak bisa membedakan "tidak ada pesan" dari "izin ditolak". limit dibatasi hingga 500 oleh aplikasi. can('smsList') bernilai 'none' di web. Pesan masuk baru bukan bagian dari wrapper ini; halaman yang ditulis manual mendapatkannya dari event appmint:sms-received (lihat listSms untuk bridge mentah). SMS adalah izin terbatas Google Play: Anda memerlukan Permissions Declaration yang disetujui di Play Console, yang biasanya hanya diberikan untuk aplikasi SMS default.

appmintNative.liveProgress appmintNative#

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

Contoh

Kartu status yang menempel dan senyap di bayangan notifikasi Android yang Anda perbarui di tempat - pengiriman, unggahan, timer, latihan. start, update, end.

Mengembalikan: start / update → Promise<boolean> (false jika tidak tersedia, jika notifikasi tidak diizinkan, atau jika pengguna mematikan kategori "Live status"-nya); end → Promise<void>. Dibutuhkan: aplikasi Android, dan izin notifikasi di Android 13+. Di web can('liveProgress') bernilai 'none' - tampilkan status yang sama di dalam aplikasi Anda di sana.

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

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

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

Timer yang berjalan: startedAt menampilkan jam yang menghitung maju dari saat itu.

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

Tangani penolakan. start menjawab false; liveProgress.lastError menjelaskan alasannya.

import { liveProgress, toast } from '@/lib/appmintNative';

export async function startDelivery() {
  const ok = await liveProgress.start({ id: 'order', title: 'Order on its way', text: 'Packing', progress: 0.1, route: '/orders' });
  if (!ok) {
    if (liveProgress.lastError === 'permission') toast('Allow notifications to follow your order here');
    else if (liveProgress.lastError === 'channel_disabled') toast('Live status is turned off in system settings');
  }
  return ok;
}

Catatan: id yang sama memperbarui satu kartu; id berbeda membuat kartu terpisah (id bawaan adalah 'live'). Pengguna tidak bisa menggesernya hilang, jadi selalu panggil end. Minta izin notifikasi lebih dulu di Android 13+ (Notification.requestPermission()): tanpa itu start menjawab false dan tidak ada yang ditampilkan.

appmintNative.LocalNotifications appmintNative#

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

Contoh

Bentuk @capacitor/local-notifications (LocalNotifications.schedule({ notifications })) untuk kode yang ditulis berdasarkan plugin Capacitor. Notifikasi tanpa schedule tampil sekarang (melalui notify()); yang dengan schedule: { at } adalah pengingat pada waktu itu, dipertahankan setelah ponsel restart.

Mengembalikan: Promise<{ notifications: { id }[] }>. Pengingat yang tidak bisa diatur (browser, build tanpa Notifications, izin ditolak) ditolak dengan alasannya - tidak pernah ditampilkan lebih awal sebagai gantinya. Pengingat berulang: scheduleAlarm atau API pengingat. Membutuhkan: Android: Notifications diaktifkan di langkah Akses (Langkah 4) saat Anda membuat build.

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

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

Beberapa notifikasi dalam satu panggilan, masing-masing menggantikan yang lebih awal yang memiliki tag.

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

export async function reportBatch(done: number, failed: number) {
  await LocalNotifications.schedule({
    notifications: [
      { title: 'Upload finished', body: `${done} files sent`, tag: 'upload-ok' },
      ...(failed ? [{ title: 'Some files failed', body: `${failed} need a retry`, tag: 'upload-failed' }] : []),
    ],
  });
}

Jika Anda perlu tahu apakah notifikasi ditampilkan, gunakan notify() dan bereaksilah pada jawabannya.

import { notify, toast } from '@/lib/appmintNative';

export async function remind(text: string) {
  const shown = await notify({ title: 'Reminder', body: text, tag: 'reminder' });
  if (!shown) toast(text);   // notifications are off: tell them in the app
}

Catatan: hanya langsung - kolom Capacitor id, schedule, at, actionTypeId dan extra tidak didukung; setiap item adalah { title, body?, icon?, tag? }. Untuk notifikasi di waktu lain, halaman yang ditulis manual memakai WebToApk.scheduleNotificationEx (Android). Untuk tombol gunakan notifyWithActions.

appmintNative.lockOrientation appmintNative#

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

Contoh

Mengunci layar ke satu orientasi untuk tampilan yang hanya masuk akal begitu - papan permainan, bagan lebar, pad tanda tangan. 'any' melepas kunci.

Mengembalikan: Promise<boolean> - true jika kunci (atau pelepasan) diterapkan, false jika platform menolak. Tidak pernah ditolak. Membutuhkan: tidak ada.

Kunci saat tampilan terbuka, lepaskan saat keluar:

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

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

Periksa jawabannya saat tampilan bergantung padanya:

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

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

Lepaskan kunci saat layar hilang, sebagai hook yang dipakai bersama beberapa layar.

import { useEffect } from 'react';
import { lockOrientation, type OrientationLock } from '@/lib/appmintNative';

export function useOrientation(mode: OrientationLock) {
  useEffect(() => {
    void lockOrientation(mode);
    return () => { void lockOrientation('any'); };
  }, [mode]);
}

// useOrientation('landscape') inside the game screen

Catatan:

  • Di aplikasi, plugin Capacitor ScreenOrientation menerapkan kunci.
  • Di web memakai screen.orientation.lock(). Kebanyakan browser menolaknya di luar layar penuh dan browser desktop selalu menolaknya, jadi harapkan false di sana.
  • Selalu beri pengguna jalan keluar dari tampilan yang terkunci.

appmintNative.longPress appmintNative#

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

Contoh

Memanggil Anda saat pengguna menahan jari pada sebuah elemen - untuk menu konteks baris, reaksi, atau aksi kartu.

Mengembalikan: fungsi berhenti berlangganan - panggil saat komponen dilepas. Dibutuhkan: tidak ada.

Opsi: ms - lama menahan, bawaan 450; haptics - haptic sedang saat aktif, bawaan true.

Buka lembar tindakan saat tekan lama, dengan tombol yang terlihat untuk menu yang sama:

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

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

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

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

Tahan yang lebih lama, tanpa haptic:

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

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

Pasangkan dengan haptic untuk konfirmasi khusus, dan bersihkan untuk setiap baris daftar.

import { useEffect, useRef } from 'react';
import { haptic, longPress } from '@/lib/appmintNative';

export function SelectableRow({ id, onSelect, children }: { id: string; onSelect(id: string): void; children: React.ReactNode }) {
  const ref = useRef<HTMLLIElement>(null);
  useEffect(() => {
    if (!ref.current) return;
    return longPress(ref.current, () => { haptic('heavy'); onSelect(id); }, { haptics: false });
  }, [id, onSelect]);
  return <li ref={ref}>{children}</li>;
}

Catatan:

  • Aktif sekali per tahanan dan menelan klik yang mengikutinya, jadi tekan-lama tidak ikut "mengetuk".
  • Menggeser lebih dari 8 px akan membatalkannya, sehingga menggulir tidak pernah memicunya. Ini juga memblokir menu tekan lama bawaan browser pada elemen tersebut.
  • Tekan lama tidak terlihat. Selalu sediakan tombol yang terlihat untuk aksi yang sama.

appmintNative.markPushRead appmintNative#

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

Contoh

Menandai satu pesan kotak masuk AppMint Push sebagai sudah dibaca, atau semua pesan jika dipanggil tanpa argumen.

Mengembalikan: boolean secara sinkron - true jika ada yang berubah. Selalu false di web. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrasi (Langkah 3) saat Anda membuat build.

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

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

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

Hitung pesan belum dibaca setelah menandai satu, untuk badge di header Anda sendiri.

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

export function openAndCount(id: string) {
  const changed = markPushRead(id);          // false on the web or when nothing changed
  const unread = pushInbox().filter((m) => !m.read).length;
  return { changed, unread };
}

Tandai semuanya sudah dibaca saat pengguna membuka layar kotak masuk dan segarkan saat pesan baru tiba.

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

export function useInbox() {
  const [items, setItems] = useState(() => pushInbox());
  useEffect(() => {
    markPushRead();
    const refresh = () => setItems(pushInbox());
    window.addEventListener('appmint:push', refresh);
    return () => window.removeEventListener('appmint:push', refresh);
  }, []);
  return items;
}

Catatan: status dibaca hanya ada di ponsel ini; notifikasi di tray tidak dihapus.

appmintNative.mediaSession appmintNative#

mediaSession — @/lib/appmintNative

Contoh

Kontrol pemutaran di layar kunci dan notifikasi untuk aplikasi yang memutar audio atau video: terbitkan apa yang sedang diputar, terima tombol Putar / Jeda / Berikutnya / Sebelumnya / Berhenti, dan serahkan video ke TV.

Mengembalikan: setMetadata, setPlaybackState, clear → void. onAction(handler) → fungsi untuk berhenti berlangganan. cast(url) → Promise<boolean> (false jika tidak ada yang bisa ditawarkan). Membutuhkan: Media Notification di Langkah 4 (Akses) saat Anda membuat build; build AI mengaktifkannya untuk Anda saat kode memakai mediaSession.

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

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

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

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

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

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

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

Kirim video saat ini ke TV:

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

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

Hapus kartu layar kunci saat pemutaran berakhir, dan biarkan saja jika platform tidak punya kontrol media.

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

export function publishEnded(audio: HTMLAudioElement): () => void {
  if (can('mediaControls') === 'none') return () => {};
  const ended = () => {
    mediaSession.setPlaybackState({ playing: false, positionMs: 0, durationMs: audio.duration * 1000 || 0 });
    mediaSession.clear();
  };
  audio.addEventListener('ended', ended);
  return () => audio.removeEventListener('ended', ended);
}

Catatan:

  • Di Android, tombol-tombol datang melalui navigator.mediaSession aplikasi, yang dipasang saat halaman selesai dimuat. onAction yang didaftarkan lebih awal (misalnya dalam efek mount) hilang, jadi daftarkan pada pemutaran pertama.
  • Aksi seek (dengan positionMs) hanya dikirim di web; notifikasi Android tidak punya seek.
  • artworkUrl harus berupa alamat https:// lengkap di internet. Di Android, cast(url) membuka pemilih "Cast ke..." (serah-terima; selesai dengan true setelah pemilih diminta). Di web memakai Remote Playback API.

appmintNative.Motion appmintNative#

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

Contoh

Bentuk @capacitor/motion di atas event standar devicemotion dan deviceorientation: gerakan dan kemiringan ponsel. Kode yang sama berjalan di aplikasi dan di browser.

Mengembalikan: Promise<{ remove(): Promise<void> }> - panggil remove() untuk berhenti. 'accel' memanggil balik dengan { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }; 'orientation' dengan { alpha, beta, gamma } (derajat). Membutuhkan: tidak ada.

Goyang untuk menyegarkan.

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

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

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

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

Miringkan gelembung level dengan event orientasi.

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

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

Bentuk hook dengan pembersihan otomatis untuk komponen React yang membaca gerakan ponsel.

import { useEffect, useState } from 'react';
import { Motion, can, type AccelEvent } from '@/lib/appmintNative';

export function useAcceleration() {
  const [g, setG] = useState({ x: 0, y: 0, z: 0 });
  const [supported] = useState(() => can('motion') !== 'none');
  useEffect(() => {
    if (!supported) return;
    let handle: { remove(): Promise<void> } | undefined;
    let cancelled = false;
    void Motion.addListener('accel', (e) => setG((e as AccelEvent).accelerationIncludingGravity)).then((h) => {
      if (cancelled) void h.remove(); else handle = h;
    });
    return () => { cancelled = true; void handle?.remove(); };
  }, [supported]);
  return { g, supported };
}

Catatan: Beberapa browser meminta akses gerak lebih dulu, jadi mulai dari ketukan di sana; jika pengguna menolak, handle tetap dikembalikan tetapi tidak ada event yang tiba. can('motion') bernilai 'none' di tempat browser tidak punya DeviceMotionEvent. Nilai sensor yang hilang tiba sebagai 0. Selalu remove() saat layar ditutup - event gerak aktif berkali-kali per detik.

appmintNative.musicVolume appmintNative#

musicVolume() — @/lib/appmintNative

Contoh

Membaca level musik latar aplikasi yang berulang, 0 sampai 100.

Mengembalikan: number, secara sinkron: 0-100, atau -1 jika tidak ada musik (di web, atau aplikasi yang dibuat tanpa file musik). Dibutuhkan: file yang dipilih di bawah Background Music pada Langkah 3 (Integrate) saat Anda membuat aplikasi.

Tampilkan penggeser musik hanya saat ada musik:

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

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

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

Selaraskan levelnya dengan sakelar "Bisukan musik", sambil mengingat volume untuk dikembalikan.

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

export function MuteMusicSwitch() {
  const before = useRef(Math.max(musicVolume(), 50));
  const [muted, setMuted] = useState(() => musicVolume() === 0);
  if (musicVolume() < 0) return null;            // no music in this build

  const toggle = () => {
    if (muted) setMusicVolume(before.current);
    else { before.current = musicVolume(); setMusicVolume(0); }
    setMuted(!muted);
  };
  return <button onClick={toggle}>{muted ? 'Unmute music' : 'Mute music'}</button>;
}

Kecilkan musik saat video diputar, lalu kembalikan.

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

export function duckMusicDuring(video: HTMLVideoElement) {
  const level = musicVolume();
  if (level < 0) return;                         // nothing to duck
  video.addEventListener('play', () => setMusicVolume(0));
  video.addEventListener('pause', () => setMusicVolume(level));
  video.addEventListener('ended', () => setMusicVolume(level));
}

Catatan: Hanya untuk aplikasi Android; web menjawab -1. Level yang diatur dengan setMusicVolume disimpan untuk peluncuran berikutnya; sampai halaman mengaturnya, levelnya adalah level build.

appmintNative.navEnd appmintNative#

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

Contoh

Didokumentasikan bersama appmintNative.navStart - contoh di sana menunjukkan yang ini juga.

navStart() menampilkan bar pemuatan tipis di atas aplikasi untuk pergantian layar yang tidak bisa dilihat aplikasi sendiri; navEnd() mengakhirinya.

Mengembalikan: tidak ada (keduanya sinkron). Membutuhkan: tidak ada. Di web (tanpa bilah shell aplikasi) keduanya tidak melakukan apa-apa.

Gunakan hanya untuk penantian saat tidak ada yang berubah di DOM sampai data tiba, dan selalu pasangkan dengan try/finally:

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

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

Bungkus pengiriman yang memakan waktu, agar pengguna melihat bar saat server bekerja.

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

export async function submitForm(data: FormData): Promise<boolean> {
  navStart();
  try {
    const res = await fetch('/api/submit', { method: 'POST', body: data });
    return res.ok;
  } catch {
    return false;            // the bar still ends in finally
  } finally {
    navEnd();
  }
}

Buat helper kecil yang membungkus promise apa pun.

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

export async function withNavBar<T>(work: Promise<T>): Promise<T> {
  navStart();
  try { return await work; } finally { navEnd(); }
}

// usage inside a click handler:
// const items = await withNavBar(loadItems(categoryId));

Catatan: Keduanya memanggil WebToApk.navStarted() / navSettled(). Aplikasi sudah menggerakkan bilah saat aplikasi Anda mengganti layar di DOM, jadi kebanyakan perpindahan rute tidak perlu apa-apa. Tidak ada yang digambar untuk perubahan yang lebih singkat dari sekitar 200 ms, dan bilah yang tidak pernah diakhiri berhenti sendiri setelah 2,5 detik. Tetap tampilkan spinner atau skeleton Anda sendiri; bilah ini hanya tambahan.

appmintNative.navStart appmintNative#

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

Contoh

navStart() menampilkan bar pemuatan tipis di atas aplikasi untuk pergantian layar yang tidak bisa dilihat aplikasi sendiri; navEnd() mengakhirinya.

Mengembalikan: tidak ada (keduanya sinkron). Membutuhkan: tidak ada. Di web (tanpa bilah shell aplikasi) keduanya tidak melakukan apa-apa.

Gunakan hanya untuk penantian saat tidak ada yang berubah di DOM sampai data tiba, dan selalu pasangkan dengan try/finally:

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

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

Bungkus pengiriman yang memakan waktu, agar pengguna melihat bar saat server bekerja.

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

export async function submitForm(data: FormData): Promise<boolean> {
  navStart();
  try {
    const res = await fetch('/api/submit', { method: 'POST', body: data });
    return res.ok;
  } catch {
    return false;            // the bar still ends in finally
  } finally {
    navEnd();
  }
}

Buat helper kecil yang membungkus promise apa pun.

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

export async function withNavBar<T>(work: Promise<T>): Promise<T> {
  navStart();
  try { return await work; } finally { navEnd(); }
}

// usage inside a click handler:
// const items = await withNavBar(loadItems(categoryId));

Catatan: Keduanya memanggil WebToApk.navStarted() / navSettled(). Aplikasi sudah menggerakkan bilah saat aplikasi Anda mengganti layar di DOM, jadi kebanyakan perpindahan rute tidak perlu apa-apa. Tidak ada yang digambar untuk perubahan yang lebih singkat dari sekitar 200 ms, dan bilah yang tidak pernah diakhiri berhenti sendiri setelah 2,5 detik. Tetap tampilkan spinner atau skeleton Anda sendiri; bilah ini hanya tambahan.

appmintNative.Network appmintNative#

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

Contoh

Status online/offline dalam bentuk plugin Network milik Capacitor - kode yang sama dengan networkStatus() / onNetworkChange().

Mengembalikan: getStatus() → Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) → bentuk Capacitor: Promise dari handle yang juga punya remove() sendiri; handler mendapat bentuk yang sama pada setiap perubahan. Dibutuhkan: tidak ada.

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

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

Baca tipe saat ini sekali dan tampilkan dalam chip status.

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

export function ConnectionChip() {
  const [label, setLabel] = useState('Checking...');
  useEffect(() => {
    void Network.getStatus().then((s) =>
      setLabel(s.connected ? (s.connectionType === 'cellular' ? 'Mobile data' : 'Wi-Fi') : 'Offline'));
  }, []);
  return <span className="chip">{label}</span>;
}

Tahan unduhan besar saat memakai data seluler, dan tangani juga kasus offline.

import { Network, toast } from '@/lib/appmintNative';

export async function startBigDownload(download: () => Promise<void>) {
  const s = await Network.getStatus();
  if (s.connectionType === 'none' || !s.connected) { toast('You are offline.'); return; }
  if (s.connectionType === 'cellular' && !window.confirm('Download over mobile data?')) return;
  await download();
}

Catatan: Nama event diabaikan - setiap listener mendapat perubahan konektivitas. connectionType adalah 'wifi', 'cellular', 'none' atau 'unknown': di aplikasi getStatus() dan listener memakai plugin Network, di web navigator.onLine / navigator.connection.type. connected berarti ponsel punya jaringan, bukan bahwa server Anda menjawab.

appmintNative.networkStatus appmintNative#

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

Contoh

Mengatakan, saat ini juga, apakah perangkat sedang online.

Mengembalikan: { connected, type }, secara sinkron. type adalah 'wifi', 'cellular', 'none' (offline) atau 'unknown'. Membutuhkan: tidak ada.

Cukup ringan untuk dipanggil saat render:

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

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

Baca nilai awal, lalu ikuti perubahannya:

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

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

Pilih ukuran gambar yang lebih ringan di data seluler, dengan kasus 'none' (offline) juga dijawab.

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

export function imageUrl(id: string): string {
  const { connected, type } = networkStatus();
  if (!connected) return '/img/placeholder.svg';         // offline: use the packaged picture
  return `/api/images/${id}?size=${type === 'cellular' ? 'small' : 'large'}`;
}

Catatan:

  • Di dalam aplikasi, ini adalah jawaban dari plugin Network (selalu diperbarui di latar belakang, jadi panggilan pertama mungkin masih membaca nilai WebView sendiri). Di web, ini adalah navigator.onLine ditambah navigator.connection.type jika browser melaporkannya, jika tidak 'unknown' - kelas kecepatan seperti '4g' tidak pernah dilaporkan sebagai tipe.
  • "Terhubung" berarti perangkat punya jaringan, bukan bahwa server Anda menjawab. Tetap tangani permintaan yang gagal.

appmintNative.nfc appmintNative#

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

Contoh

Helper satu panggilan di atas Web NFC (NDEFReader): periksa dukungan, baca tag, tulis tag teks. Aplikasi Android memakai NDEFReader-nya sendiri; di web ini adalah Web NFC bawaan Chrome (Chrome di Android).

Mengembalikan: isSupported(): Promise<boolean>; scan(onTag): Promise<() => Promise<void>> (fungsi berhenti), dan onTag menerima Array<{recordType, text, mediaType?}> per tag - nama tipe Web NFC ('text', 'url', 'mime', …) dan teks yang didekode di semua platform; write(text): Promise<boolean> (true setelah tag ditulis, false jika gagal). Dibutuhkan: di Android, aktifkan Web NFC Support di Langkah 4 (Access) saat Anda membuat aplikasi. scan() melempar error jika tidak ada NDEFReader - periksa can('nfc') lebih dulu.

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

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

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

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

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

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

Tulis tautan ke sebuah tag, dan beri tahu pengguna alasan kegagalannya jika gagal.

import { can, nfc, toast } from '@/lib/appmintNative';

export async function writeShareTag(url: string) {
  if (can('nfc') === 'none') { toast('This device cannot write NFC tags.'); return; }
  toast('Hold a blank tag to the back of the phone...');
  const ok = await nfc.write(url);
  toast(ok ? 'Tag written.' : 'Could not write the tag. Try again.');
}

Baca satu tag, lalu berhenti memindai.

import { nfc } from '@/lib/appmintNative';

export function readOnce(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    let stop: (() => Promise<void>) | undefined;
    let done = false;
    nfc.scan((records) => {
      if (done) return;
      done = true;
      void stop?.();
      resolve(records.map((r) => r.text).join(' '));
    }).then((s) => { stop = s; if (done) void s(); }, reject);
  });
}

Catatan: Di Android, isSupported() adalah true setiap kali build memiliki NFC - ini tidak menunjukkan apakah NFC dinyalakan (window.AndroidNFC.startScan() yang menunjukkan). Setelah fungsi berhenti, onTag tidak pernah dipanggil lagi. Di Android, aplikasi itu sendiri terus menangkap tag saat berada di depan (agar ketukan tidak membuka aplikasi lain), hanya saja tidak lagi meneruskannya ke Anda. text dari catatan teks tidak punya awalan bahasa; text dari catatan URL adalah URL lengkap. write() menunggu sampai tag diketuk. Untuk menulis catatan URL atau mime, dan untuk mengunci tag, gunakan NDEFReader secara langsung.

appmintNative.notify appmintNative#

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

Contoh

Menampilkan satu notifikasi sekarang dari aplikasi buatan AI, di aplikasi Android dan di web.

Mengembalikan: Promise<boolean> - true jika terkirim, false jika pengguna menolak izin notifikasi, notifikasi dimatikan untuk aplikasi, atau tidak tersedia di sini. Ini meminta izin sendiri pada pertama kali (Android 13+ dan web). Membutuhkan: Android: Notifications diaktifkan di langkah Akses (Langkah 4) saat Anda membuat build.

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

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

Minta pada saat yang tepat: notify() pertama menampilkan dialog izin. Untuk meminta dari tombol "Nyalakan peringatan" Anda sendiri - sebelum ada yang diposting - gunakan API standar, yang juga didukung aplikasi:

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

Buka layar yang tepat saat pengguna mengetuk notifikasi, dengan menggabungkan notify dengan onNotificationClick.

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { notify, onNotificationClick } from '@/lib/appmintNative';

export async function announceMessage(id: string, from: string) {
  await notify({ title: `New message from ${from}`, tag: `msg-${id}`, data: { messageId: id } });
}

export function NotificationRouter() {
  const navigate = useNavigate();
  useEffect(() => onNotificationClick((e) => {
    const { messageId } = (e.data ?? {}) as { messageId?: string };
    if (messageId) navigate(`/messages/${messageId}`);
  }), [navigate]);
  return null;
}

Catatan: Android memakai bridge notifikasi milik shell (icon menjadi ikon besar; gunakan gambar https:// atau data:); web memakai Notification API. Setelah pengguna menolak secara permanen, notify() menghasilkan false tanpa bertanya lagi - arahkan mereka ke pengaturan ponsel. Untuk tombol gunakan notifyWithActions; untuk kartu progres yang menempel gunakan liveProgress.

appmintNative.notifyWithActions appmintNative#

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

Contoh

Menampilkan notifikasi dengan hingga 3 tombol ("Tandai selesai", "Tunda"). Ketukan kembali melalui onNotificationAction.

Mengembalikan: Promise<boolean> - true jika terkirim, false jika izin notifikasi ditolak (meminta pertama kali, seperti notify()). Membutuhkan: Android: Notifications diaktifkan di langkah Akses (Langkah 4) saat Anda membuat build, dan izin pengguna di Android 13+. Di web can('notificationActions') adalah 'none'.

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

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

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

Timer selesai: satu tombol menambah lima menit, yang lain menghentikannya.

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

export async function timerDone() {
  if (can('notificationActions') === 'none') return false;
  return notifyWithActions({
    title: 'Timer finished',
    tag: 'timer',
    actions: [{ id: 'more', title: '+5 min' }, { id: 'stop', title: 'Stop' }],
  });
}

export const stopTimerButtons = onNotificationAction((e) => {
  if (e.tag !== 'timer') return;                 // ignore other notifications' buttons
  if (e.actionId === 'more') startTimer(5);
});

Tangani izin yang ditolak setelah panggilan, dan bersihkan di dalam komponen.

import { useEffect, useState } from 'react';
import { notifyWithActions, onNotificationAction } from '@/lib/appmintNative';

export function ApprovalRequest({ requestId }: { requestId: string }) {
  const [note, setNote] = useState('');
  useEffect(() => onNotificationAction((e) => {
    if (e.tag === `req-${requestId}`) setNote(e.actionId === 'approve' ? 'Approved' : 'Rejected');
  }), [requestId]);

  const ask = async () => {
    const posted = await notifyWithActions({
      title: 'Approve request?', tag: `req-${requestId}`,
      actions: [{ id: 'approve', title: 'Approve' }, { id: 'reject', title: 'Reject' }],
    });
    if (!posted) setNote('Notifications are off, so no buttons were shown.');
  };
  return <button onClick={ask}>{note || 'Ask me'}</button>;
}

Catatan: Setiap tombol menampilkan title-nya; id-nya adalah yang kembali sebagai e.actionId. Opsi input (kotak balasan sebaris) tidak didukung di aplikasi ini - diabaikan, tombol tidak punya kotak balasan dan e.text selalu ''. Ketukan hanya sampai ke halaman saat aplikasi berjalan. Di web wrapper akan memposting notifikasi biasa tanpa tombol, itulah sebabnya contoh memeriksa can() lebih dulu.

appmintNative.offlineBanner appmintNative#

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

Contoh

Memasang strip siap pakai yang meluncur di bawah bilah status saat koneksi putus, dan sebentar menampilkan "Kembali online" saat kembali.

Mengembalikan: fungsi berhenti berlangganan yang menghapus banner dan berhenti mendengarkan. Membutuhkan: tidak ada.

Pasang sekali saat aplikasi dimulai:

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

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

Kata-kata Anda sendiri (misalnya dalam bahasa aplikasi):

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

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

Pasang banner hanya untuk area aplikasi yang sudah masuk, dan hapus saat keluar.

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

export function useOfflineBanner(signedIn: boolean) {
  useEffect(() => {
    if (!signedIn) return;
    return offlineBanner({ message: 'Offline: changes will sync later' });
  }, [signedIn]);
}

Catatan:

  • Bawaan: "Anda sedang offline" dan "Kembali online". Pesan online hilang setelah sekitar 1,8 detik; pesan offline tetap ada sampai koneksi kembali.
  • Jika perangkat sudah offline saat dipasang, banner langsung tampil.
  • Dibangun di atas onNetworkChange(), jadi nyata di setiap permukaan. Warna berasal dari token aplikasi (--ink / --bg offline, --accent / --accent-ink online). Punya role="status" untuk pembaca layar.
  • Pasang sekali saja. Dua panggilan menggambar dua banner.

appmintNative.onAlarm appmintNative#

onAlarm(cb) — @/lib/appmintNative

Contoh

Mendengarkan hasil alarm: aplikasi yang dibuka oleh alarm (action: 'open', dengan ringing), dan bagaimana dering berakhir - dismiss, snooze, missed (berdering sampai habis, atau digantikan alarm lain) atau error. Hasil dari saat aplikasi tertutup dikirim saat halaman dimuat berikutnya.

Mengembalikan: fungsi yang berhenti mendengarkan. Callback menerima { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. Membutuhkan: 🔔 Alarm clock di langkah Akses (Langkah 4) saat Anda membuat build.

import { useEffect, useState } from 'react';
import { onAlarm } from '@/lib/appmintNative';

export function AlarmHost() {
  const [ringingId, setRingingId] = useState<string | null>(null);
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open' && e.ringing) setRingingId(e.id);
    if (e.action === 'dismiss' || e.action === 'snooze' || e.action === 'missed') setRingingId(null);
  }), []);
  return ringingId ? <RingScreen id={ringingId} onClose={() => setRingingId(null)} /> : null;
}

Atur alarm, dengan menjawab alarmSupport() lebih dulu.

import { alarmSupport, scheduleAlarm } from '@/lib/appmintNative';

export function setWakeUp(at: Date): string {
  const support = alarmSupport();
  if (support === 'unavailable') return 'Alarms are only available in the Android app.';
  if (support !== 'ready') return `Alarm needs attention first (${support}).`;
  const ok = scheduleAlarm('wake-up', { at: at.getTime(), title: 'Wake up', repeat: 'weekdays', snoozeMinutes: 10 });
  return ok ? 'Alarm set.' : 'Could not set the alarm.';
}

Hentikan atau tunda alarm yang berbunyi dari layar Anda sendiri, dan bereaksi terhadap hasilnya.

import { useEffect, useState } from 'react';
import { onAlarm, stopAlarm, snoozeAlarm, isAlarmRinging } from '@/lib/appmintNative';

export function RingControls() {
  const [ringing, setRinging] = useState(() => isAlarmRinging());
  const [last, setLast] = useState('');
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open') setRinging(!!e.ringing);
    else { setRinging(false); setLast(e.action === 'error' ? `Alarm error: ${e.error}` : `Alarm ${e.action}`); }
  }), []);
  if (!ringing) return <p>{last}</p>;
  return (
    <div>
      <button onClick={() => stopAlarm()}>Stop</button>
      <button onClick={() => snoozeAlarm('', 5)}>Snooze 5 min</button>
    </div>
  );
}

Catatan: pengingat biasa yang membuka aplikasi juga tiba di sini dengan action: 'open' dan ringing: false.

appmintNative.onBackGesture appmintNative#

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

Contoh

Membiarkan lembar, modal, lightbox, atau langkah wizard yang terbuka menjawab gestur KEMBALI lebih dulu - usapan tepi / tombol kembali Android di aplikasi, tombol kembali browser di web.

Mengembalikan: fungsi berhenti berlangganan - panggil saat tampilan ditutup atau dilepas. Handler Anda mengembalikan true untuk menghabiskan penekanan (Anda menutup sesuatu) atau false untuk meneruskannya. Membutuhkan: tidak ada.

Daftarkan hanya saat sheet terbuka:

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

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

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

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

Mundur melalui wizard, dan biarkan tombol kembali pada langkah terakhir keluar seperti biasa:

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

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

Tutup lightbox dengan gestur kembali, dengan pendaftaran terikat pada gambar yang sedang terbuka.

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

export function Lightbox({ src, onClose }: { src: string | null; onClose: () => void }) {
  useEffect(() => {
    if (!src) return;
    return onBackGesture(() => { onClose(); return true; });
  }, [src, onClose]);
  return src ? <img src={src} alt="" onClick={onClose} /> : null;
}

Catatan:

  • Penangan membentuk tumpukan: yang didaftarkan terakhir (lembar paling atas) ditanya lebih dulu. Penangan yang melempar kesalahan tidak menghabiskan apa pun.
  • Di Android ini melewati handler kembali milik shell. Tanpa ini, menggeser kembali saat lembar terbuka akan menutup seluruh aplikasi.
  • Di web, ini mendorong satu entri riwayat selama ada handler terdaftar dan menghapusnya saat yang terakhir berhenti berlangganan.
  • Tetap tampilkan kontrol kembali / tutup yang terlihat: banyak pengguna tidak pernah memakai gestur kembali.
  • React Router sudah menangani tombol kembali halaman biasa; gunakan ini hanya untuk state yang tidak diketahui router. Selalu batalkan langganan, atau sheet yang sudah ditutup akan terus menangkap tombol kembali.

appmintNative.onInAppMessage appmintNative#

import { onInAppMessage } from '@/lib/appmintNative'

Contoh

Memanggil Anda untuk setiap pesan dalam aplikasi (kampanye pembuat) yang ditampilkan, diklik, ditutup, atau dijawab pada pemasangan ini. Mengembalikan fungsi berhenti berlangganan.

Mengembalikan: () => void. Callback menerima {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. Tidak pernah aktif di web. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi.

import { useEffect } from 'react';
import { onInAppMessage } from '@/lib/appmintNative';

export function useInAppAnalytics(log: (event: string, name: string) => void) {
  useEffect(() => onInAppMessage((e) => log('inapp_' + e.type, e.name)), [log]);
}

Gambar sendiri pesannya (dengan setInAppPresentation('none'), presented bernilai false dan message membawa desainnya):

import { useEffect, useState } from 'react';
import { onInAppMessage, setInAppPresentation, type InAppMessageEvent } from '@/lib/appmintNative';

export function useOwnInAppDialog() {
  const [msg, setMsg] = useState<InAppMessageEvent['message'] | null>(null);
  useEffect(() => {
    setInAppPresentation('none');
    const off = onInAppMessage((e) => { if (e.type === 'shown' && !e.presented && e.message) setMsg(e.message); });
    return () => { off(); setInAppPresentation('auto'); };
  }, []);
  return { msg, close: () => setMsg(null) };
}

Tanggapi tombol yang diketuk pengguna pada gambar milik aplikasi sendiri:

import { onInAppMessage } from '@/lib/appmintNative';

onInAppMessage((e) => {
  if (e.type === 'clicked' && e.link === 'appmint://notifications/enable') refreshNotificationBadge();
});

Catatan: button adalah -1 untuk ketukan pada pesan itu sendiri, jika tidak maka indeks tombol.

Jawaban (type: 'answered'). Pesan dengan baris bintang (Star survey, Rate us) atau chip skor 0-10 (NPS score) melaporkan jawabannya sekali, saat ditutup: question adalah 'stars' (nilai 1-5) atau 'nps' (nilai 0-10), dan template menyebut template galeri asalnya. Pusat pengiriman menampilkan rata-rata atau skor NPS; kode Anda bisa menindaklanjuti:

import { onInAppMessage } from '@/lib/appmintNative';

onInAppMessage((e) => {
  if (e.type === 'answered' && e.question === 'nps' && (e.value ?? 10) <= 6) openFeedbackSheet();
});

Simpan sendiri catatan peringkat bintang:

import { onInAppMessage } from '@/lib/appmintNative';

onInAppMessage((e) => {
  if (e.type !== 'answered' || e.question !== 'stars') return;
  void fetch('/api/ratings', { method: 'POST', body: JSON.stringify({ stars: e.value, campaign: e.name }) });
});

Ucapkan terima kasih kepada orang yang memberi nilai tinggi, di komponen React:

import { useEffect, useState } from 'react';
import { onInAppMessage } from '@/lib/appmintNative';

export function useHappyUser() {
  const [happy, setHappy] = useState(false);
  useEffect(() => onInAppMessage((e) => {
    if (e.type === 'answered' && ((e.question === 'stars' && (e.value ?? 0) >= 4) || (e.question === 'nps' && (e.value ?? 0) >= 9))) setHappy(true);
  }), []);
  return happy;
}

appmintNative.onKeyboard appmintNative#

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

Contoh

Memberi tahu Anda berapa piksel halaman yang tertutup keyboard di layar, setiap kali itu berubah, sehingga bilah bawah bisa naik di atasnya.

Mengembalikan: fungsi untuk berhenti berlangganan - panggil saat unmount. Callback menerima angka dalam piksel CSS: padding bawah tambahan yang Anda butuhkan, 0 saat keyboard tertutup. Membutuhkan: tidak ada.

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

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

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

Jaga daftar chat tetap tergulir ke pesan terbaru saat keyboard terbuka:

import { useEffect, useRef } from 'react';
import { onKeyboard } from '@/lib/appmintNative';

export function MessageList({ children }: { children: React.ReactNode }) {
  const endRef = useRef<HTMLDivElement>(null);
  useEffect(() => onKeyboard((inset) => {
    if (inset > 0) endRef.current?.scrollIntoView({ block: 'end' });   // keyboard just opened
  }), []);
  return <div className="overflow-y-auto">{children}<div ref={endRef} /></div>;
}

Hook yang bisa dipakai ulang, dengan celah area aman yang hilang saat keyboard terbuka:

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

export function useKeyboardInset(): number {
  const [inset, setInset] = useState(0);        // 0 until the keyboard first opens
  useEffect(() => onKeyboard(setInset), []);
  return inset;
}

// In a component: the home-indicator gap is only needed when the keyboard is closed.
// const kb = useKeyboardInset();
// <footer style={{ paddingBottom: kb > 0 ? kb : 'var(--safe-bottom)' }}>...</footer>

Catatan:

  • Di aplikasi, ini mendengarkan plugin Keyboard Capacitor dan hanya melaporkan bagian keyboard yang belum diserap tata letak. Shell biasanya mengubah ukuran halaman untuk keyboard, jadi nilainya sering 0 di sana - itu benar; jangan tambahkan tinggi keyboard mentah sendiri.
  • Di aplikasi layar penuh atau kiosk (halaman tidak diubah ukurannya), ini melaporkan seluruh tinggi keyboard.
  • Di web memakai visualViewport, memanggil callback sekali segera, dan menganggap di bawah 80 px sebagai chrome browser (dilaporkan sebagai 0).
  • Di aplikasi panggilan pertama datang saat keyboard pertama kali terbuka, jadi mulai state Anda di 0.

appmintNative.onNetworkChange appmintNative#

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

Contoh

Memanggil Anda setiap kali koneksi putus atau kembali - untuk banner offline, atau untuk mencoba ulang penulisan yang antre.

Mengembalikan: fungsi berhenti berlangganan - panggil saat komponen dilepas. Dibutuhkan: tidak ada.

Coba lagi pekerjaan yang mengantre saat perangkat kembali online:

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

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

Tampilkan status saat ini sekaligus, lalu ikuti perubahannya (callback tidak dipanggil saat berlangganan):

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

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

Tahan unduhan besar di data seluler, dan anggap 'none' sebagai offline:

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

export function useDataSaver(setHeavyAllowed: (ok: boolean) => void) {
  useEffect(() => onNetworkChange((s) => {
    if (s.type === 'none' || !s.connected) { toast('You are offline'); setHeavyAllowed(false); return; }
    setHeavyAllowed(s.type !== 'cellular');   // 'wifi' and 'unknown' may download freely
  }), [setHeavyAllowed]);
}

Catatan:

  • Ini TIDAK memanggil Anda dengan status saat ini saat Anda berlangganan. Baca networkStatus() untuk nilai awalnya.
  • Di aplikasi, ini mendengarkan plugin Capacitor Network (networkStatusChange); type adalah 'wifi', 'cellular', 'none' atau 'unknown'.
  • Di web, ini mendengarkan event online / offline dan melaporkan networkStatus().
  • Untuk banner siap pakai, gunakan offlineBanner(), yang dibangun di atas ini.

appmintNative.onNotificationAction appmintNative#

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

Contoh

Mendengarkan ketukan pada tombol notifikasi (dari notifyWithActions).

Mengembalikan: fungsi yang berhenti mendengarkan. Callback menerima { actionId, tag } - ID tombol dan tag notifikasi. Aplikasi Android tidak mengirim balasan text (balasan sebaris tidak didukung), jadi jangan mengandalkan e.text. Membutuhkan: sama seperti notifyWithActions.

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

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

Event mentah di halaman tulisan tangan (aplikasi Android):

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

Kirim notifikasi dengan tombol hanya di tempat build mendukungnya, dan tangani ketukan berdasarkan tag notifikasi:

import { useEffect } from 'react';
import { can, notifyWithActions, onNotificationAction, toast } from '@/lib/appmintNative';

export async function remindWithButtons(taskId: string, title: string) {
  if (can('notificationActions') === 'none') { toast(title); return; }   // no buttons here
  await notifyWithActions({
    title, body: 'Tap a button', tag: `task-${taskId}`,
    actions: [{ id: 'done', title: 'Done' }, { id: 'later', title: 'Later' }],
  });
}

export function useTaskActions(complete: (id: string) => void, snooze: (id: string) => void) {
  useEffect(() => onNotificationAction(({ actionId, tag }) => {
    const id = tag.replace(/^task-/, '');
    if (actionId === 'done') complete(id);
    else if (actionId === 'later') snooze(id);
  }), [complete, snooze]);
}

Catatan: di aplikasi Android ini adalah event appmint:notification-action (juga window.onAppMintNotificationAction(detail)); di web tidak melakukan apa-apa. Ketukan hanya diterima saat aplikasi berjalan - mengetuk tombol tidak membuka aplikasi.

appmintNative.onNotificationClick appmintNative#

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

Contoh

Menjalankan callback saat pengguna mengetuk notifikasi yang dikirim aplikasi - termasuk ketukan yang memulai aplikasi dari kondisi mati - agar aplikasi bisa membuka layar yang tepat.

Mengembalikan: fungsi berhenti berlangganan. Callback mendapat { tag, title, body, data } - data adalah yang Anda berikan ke notify({ …, data }). Membutuhkan: notifikasi, seperti notify(). Event di baliknya: appmint:notification-click.

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

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

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

Buka layar yang tepat setelah ketukan, bahkan saat ketukan itu memulai aplikasi dari kondisi mati, dan abaikan notifikasi yang tidak membawa data:

import { useEffect } from 'react';
import { onNotificationClick } from '@/lib/appmintNative';

export function useNotificationRouting(navigate: (path: string) => void) {
  useEffect(() => onNotificationClick(({ tag, data }) => {
    const d = data as { screen?: string; id?: string } | null;
    if (!d?.screen) { console.log('plain notification tapped:', tag); return; }
    navigate(d.id ? `/${d.screen}/${d.id}` : `/${d.screen}`);
  }), [navigate]);
}

Kirim notifikasi hanya di tempat notifikasi tersedia, dengan data untuk ketukan (data membutuhkan tag):

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

export async function announceMessage(chatId: string, from: string, text: string) {
  if (can('notify') === 'none') return;                 // nothing to tap on this surface
  const shown = await notify({ title: from, body: text, tag: `chat-${chatId}`, data: { screen: 'chat', id: chatId } });
  if (!shown) toast('Notifications are turned off');
}

Catatan: Ketukan berjalan melalui splash dan layar PIN ke halaman. data harus berupa JSON (di bawah 16 KB) dan memerlukan tag.

appmintNative.onPullToRefresh appmintNative#

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

Contoh

Menambahkan gestur tarik-ke-bawah-untuk-menyegarkan ke daftar yang bisa digulir: tarik ke bawah di atas, lepas, dan data Anda dimuat ulang.

Mengembalikan: fungsi berhenti berlangganan - panggil saat komponen dilepas. Dibutuhkan: tidak ada.

el adalah elemen yang digulir (atau document.documentElement saat seluruh halaman digulir). Spinner tetap tampil sampai promise dari onRefresh selesai:

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

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

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

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

Halaman yang di-scroll secara keseluruhan, dengan tarikan lebih panjang:

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

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

Segarkan daftar melalui data hook, dengan ketukan haptic saat data sudah tiba:

import { useEffect, useRef } from 'react';
import { onPullToRefresh, haptic } from '@/lib/appmintNative';

export function useListRefresh(refetch: () => Promise<unknown>) {
  const ref = useRef<HTMLUListElement>(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    return onPullToRefresh(el, async () => {
      await refetch();
      haptic('success');                     // the spinner stays until this promise settles
    }, { threshold: 80 });
  }, [refetch]);
  return ref;                                // <ul ref={ref} className="h-full overflow-y-auto">
}

Catatan:

  • Hanya dimulai saat daftar digulir sampai paling atas. threshold bawaan adalah tarikan 72 px.
  • Ini menggambar indikator bulat dengan warna aplikasi (--surface, --accent), memberi getaran sedang pada titik penentuan, dan mengabaikan tarikan baru saat penyegaran sedang berjalan.
  • Ini menyegarkan DATA Anda, bukan halaman. Tarik-untuk-muat-ulang halaman milik shell (Enable Pull-to-Refresh di Langkah 3 (Integrate)) mati secara default - biarkan mati saat memakai ini, agar tarikan tidak ikut memuat ulang halaman.
  • Dibangun di atas Pointer Events, sehingga sama di setiap permukaan. Ini mengatur overscroll-behavior-y: contain pada el dan mengembalikannya saat berhenti berlangganan.

appmintNative.onShared appmintNative#

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

Contoh

Memanggil Anda saat aplikasi lain membagikan teks atau tautan ke aplikasi ini ketika aplikasi sudah terbuka.

Mengembalikan: fungsi berhenti berlangganan - panggil saat dilepas. Membutuhkan: opsi build Receive Shares From Other Apps dengan "Teks dan tautan" - diaktifkan otomatis saat kode Anda memanggil onShared( atau sharedText(.

Tangani kedua kasus - berbagi yang memulai aplikasi, dan berbagi yang datang kemudian:

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

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

Halaman yang ditulis tangan bisa mendengarkan event shell sendiri:

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

Isi awal formulir "catatan baru" dari tautan yang dibagikan, hanya di tempat build bisa menerima kiriman berbagi:

import { useEffect, useState } from 'react';
import { can, onShared, toast } from '@/lib/appmintNative';

export function NewNote() {
  const [body, setBody] = useState('');
  useEffect(() => {
    if (can('shareTarget') === 'none') return;
    return onShared((p) => {
      setBody([p.subject, p.text, p.url].filter(Boolean).join('\n'));
      toast('Shared text added');
    });
  }, []);
  return <textarea value={body} onChange={(e) => setBody(e.target.value)} />;
}

Catatan:

  • Android: mendengarkan event appmint:shared dari shell; shell juga memanggil window.onAppMintShared(detail) jika halaman mendefinisikannya. Payload yang sama juga disimpan untuk sharedText() sampai clearShared().
  • Di web tidak pernah aktif dan mengembalikan fungsi berhenti berlangganan yang tidak melakukan apa-apa.

appmintNative.onVolumeChange appmintNative#

onVolumeChange(cb) — @/lib/appmintNative

Contoh

Memanggil cb setiap kali salah satu level volume ponsel berubah - tombol fisik, penggeser sistem, aplikasi lain.

Mengembalikan: fungsi yang berhenti mendengarkan (yang terakhir berhenti mematikan pemantauan native). Callback mendapat { stream, current, max, percent, muted? } untuk setiap stream yang berubah. Tidak melakukan apa-apa di web. Membutuhkan: tidak ada.

import { useEffect, useState } from 'react';
import { getVolume, onVolumeChange } from '@/lib/appmintNative';

export function MediaVolumeBadge() {
  const [pct, setPct] = useState(() => getVolume('media')?.percent ?? null);
  useEffect(() => onVolumeChange((v) => { if (v.stream === 'media') setPct(v.percent); }), []);
  return pct === null ? null : <span>🔊 {pct}%</span>;
}

Beri peringatan saat volume media diturunkan sampai habis saat video diputar, dan berhenti mendengarkan saat dilepas:

import { useEffect } from 'react';
import { onVolumeChange, toast } from '@/lib/appmintNative';

export function useQuietWarning(playing: boolean) {
  useEffect(() => {
    if (!playing) return;
    return onVolumeChange((v) => {
      if (v.stream === 'media' && (v.percent === 0 || v.muted)) toast('Volume is off');
    });
  }, [playing]);
}

Cerminkan level ponsel pada slider dan tulis kembali perubahan pengguna (web tidak punya volume, jadi getVolume bernilai null di sana):

import { useEffect, useState } from 'react';
import { getVolume, setVolume, onVolumeChange } from '@/lib/appmintNative';

export function MediaVolumeSlider() {
  const [pct, setPct] = useState<number | null>(() => getVolume('media')?.percent ?? null);
  useEffect(() => onVolumeChange((v) => { if (v.stream === 'media') setPct(v.percent); }), []);
  if (pct === null) return null;                       // not the Android app
  return (
    <input type="range" min={0} max={100} value={pct}
      onChange={(e) => { const r = setVolume('media', Number(e.target.value)); if (r.ok) setPct(Number(e.target.value)); }} />
  );
}

appmintNative.openNotificationSettings appmintNative#

openNotificationSettings(): Promise<boolean>

Contoh

Membuka layar pengaturan notifikasi aplikasi ini sendiri (aplikasi Android).

Mengembalikan: Promise<boolean> - true jika layar terbuka; false di web dan di mana pun yang tidak punya layar seperti itu. Periksa can('notificationSettings') sebelum menggambar kontrolnya. Membutuhkan: tidak ada.

import { can, openNotificationSettings } from '@/lib/appmintNative';

export function NotificationSettingsRow(row: HTMLElement) {
  row.hidden = can('notificationSettings') === 'none';
  row.addEventListener('click', () => { void openNotificationSettings(); });
}

Catatan: untuk "Aktifkan notifikasi" satu ketukan, gunakan enableNotifications(), yang membuka layar ini hanya saat dialog izin tidak bisa ditampilkan lagi.

appmintNative.passkeys appmintNative#

passkeys — available / create / get / origin / assetLinks

Contoh

Masuk dan pendaftaran dengan passkey menggunakan opsi JSON yang dibuat server passkey Anda. Di aplikasi Android memakai pengelola passkey Android; di web memakai navigator.credentials.

Mengembalikan: available() → 'native' | 'web' | 'none'; create(optionsJSON) → Promise dari RegistrationResponseJSON; get(optionsJSON) → Promise dari AuthenticationResponseJSON (kirim keduanya kembali ke server Anda dengan POST). Keduanya menolak dengan DOMException (NotAllowedError, InvalidStateError, SecurityError, NotSupportedError). origin() → string, assetLinks() → array (hanya di aplikasi; '' / [] di web). Membutuhkan: server passkey; di aplikasi juga Android 9+ dengan layanan Google Play dan milik domain Anda assetlinks.json.

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

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

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

Nilai penyiapan satu kali, dari aplikasi yang dipasang lewat Google Play:

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

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

Tawarkan tombol passkey hanya di tempat yang bisa berfungsi, dan katakan jika tidak bisa:

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

export function PasskeyButton({ onClick }: { onClick: () => void }) {
  const kind = passkeys.available();          // 'native' | 'web' | 'none'
  if (kind === 'none') return <p>Passkeys are not available on this device.</p>;
  return <button onClick={onClick}>{kind === 'native' ? 'Use your phone passkey' : 'Use a passkey'}</button>;
}

Catatan: Di web, ini membutuhkan halaman aman (https://). Halaman yang dibundel di aplikasi tidak punya domain: opsi dari server Anda harus mengatur rp.id / rpId ke domain Anda. Isi otomatis passkey (mediation: 'conditional') tidak tersedia di aplikasi - mulai dari sebuah tombol.

appmintNative.paste appmintNative#

paste(): Promise<string>

Contoh

Membaca teks di clipboard sistem.

Mengembalikan: Promise<string> - teks clipboard, atau '' jika clipboard kosong. DITOLAK jika browser ini sama sekali tidak bisa membaca clipboard, atau jika pengguna menolak izin. Dibutuhkan: tidak ada.

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

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

Bentuk bergaya Capacitor:

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

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

Tempel ke kotak teks dengan pesan untuk setiap hasil, dan pasangkan dengan copy:

import { copy, paste, toast } from '@/lib/appmintNative';

export async function pasteInto(setValue: (s: string) => void) {
  try {
    const text = await paste();
    if (text) setValue(text);
    else toast('Nothing to paste');
  } catch {
    toast('Allow clipboard access to paste');
  }
}

export async function shoutClipboard() {
  const text = await paste().catch(() => '');
  if (text && await copy(text.toUpperCase())) toast('Converted and copied');
}

Catatan: Di aplikasi plugin Clipboard membaca teksnya; clipboard kosong menghasilkan '', bukan kesalahan. Di browser memakai navigator.clipboard.readText() (hanya Chromium), yang meminta izin pengguna - panggil dari ketukan.

appmintNative.pickContact appmintNative#

pickContact(): Promise<Contact | null>

Contoh

Membuka pemilih kontak sistem agar pengguna memilih satu orang: pemilih Android di aplikasi, Contact Picker API di Chrome pada Android.

Mengembalikan: Promise<Contact | null> - null saat pengguna membatalkan atau akses ditolak. Membutuhkan: tidak ada (pemilih sistem tidak membutuhkan izin).

Pilih seseorang dan isi formulir. Kontak punya bentuk yang sama di aplikasi dan di web: { id?, name?, phones?: string[], emails?: string[], photoUri? }.

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

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

Tombol "Pilih kontak" yang menampilkan pilihan, dan kotak nomor biasa jika pemilih tidak ada:

import { useState } from 'react';
import { can, pickContact, type Contact } from '@/lib/appmintNative';

export function InvitePicker() {
  const [who, setWho] = useState<Contact | null>(null);
  if (can('contactPick') === 'none') return <input placeholder="Phone number" />;   // type it by hand
  return (
    <div>
      <button onClick={async () => setWho((await pickContact()) ?? who)}>Choose a contact</button>
      {who && <p>{who.name} {who.phones?.[0]}</p>}
    </div>
  );
}

Serahkan nomor yang dipilih ke aplikasi pesan, dengan pesan jika kontak tidak punya nomor:

import { pickContact, toast } from '@/lib/appmintNative';

export async function textAFriend(message: string) {
  const c = await pickContact();
  if (!c) return;                              // cancelled
  const phone = c.phones?.[0];
  if (!phone) { toast(`${c.name ?? 'That contact'} has no phone number`); return; }
  window.location.href = `sms:${encodeURIComponent(phone)}?body=${encodeURIComponent(message)}`;
}

Catatan: Di aplikasi hanya nomor yang diketuk yang kembali, tidak pernah email, dan photoUri adalah alamat content:// foto kontak. can('contactPick') bernilai 'none' di browser tanpa Contact Picker API (desktop, Safari) - jangan tampilkan tombolnya di sana.

appmintNative.pickFile appmintNative#

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

Contoh

Memungkinkan pengguna memilih satu file dan membacanya sebagai teks. Di aplikasi, ini membuka pemilih dokumen Android; di web, ini membuka input file biasa.

Mengembalikan: Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }, atau null hanya saat pengguna membatalkan. Ini menolak dengan Error yang code-nya adalah 'no-picker' (ponsel tidak punya aplikasi pemilih dokumen), 'disabled' atau 'failed' jika tidak ada pemilih yang bisa dibuka. text() membaca seluruh file dan menolak dengan kode jembatan (misalnya 'not-found') jika tidak bisa. Membutuhkan: tidak ada. Dengan Native Folder Access (SAF) dicentang di Langkah 4 (Akses), aplikasi memakai pemilih dokumen Android; tanpa itu, aplikasi memakai pemilih file milik halaman.

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

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

Impor cadangan JSON dan tangani setiap cara pickFile bisa gagal:

import { pickFile, toast } from '@/lib/appmintNative';

export async function restoreBackup(apply: (data: unknown) => void) {
  try {
    const f = await pickFile('application/json');
    if (!f) return;                                    // cancelled
    apply(JSON.parse(await f.text()));
    toast(`Restored ${f.name}`);
  } catch (e) {
    const code = (e as { code?: string }).code;
    if (code === 'no-picker') toast('This phone has no file picker app');
    else if (e instanceof SyntaxError) toast('That file is not a valid backup');
    else toast('Could not open the file');
  }
}

Pilih file teks dan masukkan ke editor, di samping detail file:

import { useState } from 'react';
import { pickFile } from '@/lib/appmintNative';

export function OpenText() {
  const [doc, setDoc] = useState<{ name: string; kb: number; text: string } | null>(null);
  return (
    <div>
      <button onClick={async () => {
        const f = await pickFile('text/*');
        if (f) setDoc({ name: f.name, kb: Math.ceil(f.size / 1024), text: await f.text() });
      }}>Open a text file</button>
      {doc && <><h3>{doc.name} ({doc.kb} KB)</h3><textarea defaultValue={doc.text} /></>}
    </div>
  );
}

Catatan: Di aplikasi, text() membaca file dalam potongan 4 MB sampai akhir dan mendekodenya sebagai UTF-8, jadi file besar dibaca penuh - tetapkan sendiri batas ukuran yang sanggup ditangani layar. Pemilih yang tidak bisa dibuka akan ditolak, jadi bungkus pickFile() dengan try/catch juga. Di web text() adalah File.text() bawaan browser. Untuk file biner atau menulis kembali, halaman tulisan tangan memakai WebToApkFS.

appmintNative.pip appmintNative#

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

Contoh

Menjaga video tetap diputar di jendela kecil mengambang saat pengguna melakukan hal lain.

Mengembalikan: enter() mengembalikan Promise<boolean> - true saat jendela mengambang terbuka. exit() mengembalikan Promise<void>. Keduanya tidak pernah menolak. Membutuhkan: di aplikasi Android, opsi Picture-in-Picture (PiP) pada build di Langkah 3 (Integrasi) - opsi ini diaktifkan otomatis saat kode Anda memanggil pip.enter(.

Tombol "perkecil" pada pemutar:

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

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

Lepaskan saat pemutar ditutup (web):

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

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

Masuk ke picture-in-picture otomatis saat pengguna meninggalkan video yang sedang diputar, hanya di tempat yang didukung:

import { useEffect, type RefObject } from 'react';
import { can, pip } from '@/lib/appmintNative';

export function useAutoPip(videoRef: RefObject<HTMLVideoElement>) {
  useEffect(() => {
    if (can('pip') === 'none') return;
    const onHide = () => {
      const v = videoRef.current;
      if (document.hidden && v && !v.paused) void pip.enter(v);
    };
    document.addEventListener('visibilitychange', onHide);
    return () => document.removeEventListener('visibilitychange', onHide);
  }, [videoRef]);
}

Catatan:

  • Android (8+): SELURUH aplikasi mengecil ke dalam jendela mengambang, dan argumen video diabaikan. Mengembalikan false di Android lama atau pada build tanpa opsi PiP. Pengguna memperbesar jendela untuk kembali; exit() tidak melakukan apa-apa di sana.
  • Web: picture-in-picture milik elemen <video> itu sendiri. Video harus sudah dimuat (biasanya sedang diputar); panggilan tanpa video mengembalikan false.
  • Panggil dari sebuah ketukan.

appmintNative.platform appmintNative#

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

Contoh

Memberi tahu di permukaan mana kode berjalan: aplikasi Android yang terinstal, atau browser web.

Mengembalikan: sebuah string, langsung: 'android' di dalam aplikasi, 'web' di browser. Ini bertanya ke Capacitor lebih dulu (Capacitor.getPlatform()), lalu memeriksa bridge shell Android, jika tidak 'web'. Tipenya juga mencantumkan 'ios', tetapi aplikasi AppMint tidak pernah mendapatkannya - AppMint hanya membuat aplikasi Android. Dibutuhkan: tidak ada.

Pilih tautan yang tepat dan salin.

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

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

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

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

Tampilkan kontrol khusus aplikasi.

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

export function InstallBanner() {
  // In the installed app there is nothing to install.
  if (platform() === 'android') return null;
  return <a href="https://play.google.com/store/apps/details?id=com.example.app">Get the Android app</a>;
}

Beri tag event analitik dengan permukaannya.

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

export function track(event: string, data: Record<string, unknown> = {}) {
  return fetch('/api/track', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ event, surface: platform(), ...data }),
  });
}

Catatan: platform() tidak pernah menjawab 'web' di dalam aplikasi yang terpasang. Jangan pakai ini untuk memutuskan apakah suatu fitur ada - banyak fitur juga bergantung pada sakelar build; can('<capability>') yang menjawabnya.

appmintNative.privacyScreen appmintNative#

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

Contoh

Memblokir tangkapan layar, perekaman layar, dan pratinjau pengalih aplikasi selagi layar berisi rahasia terbuka.

Mengembalikan: enable() → Promise<boolean> (false jika tidak ada yang bisa memblokirnya); disable() → Promise<void>; isEnabled() → Promise<boolean>. Dibutuhkan: tidak ada. Di aplikasi Android ini memakai pemblokir tangkapan layar milik shell (tangkapan layar, perekaman layar, casting, dan thumbnail aplikasi terbaru).

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

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

Beri tahu pengguna apa yang dilindungi - hanya jika memang begitu:

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

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

Aktifkan perlindungan untuk satu tampilan sensitif dan beri tahu pengguna jika tidak bisa diterapkan:

import { privacyScreen, toast } from '@/lib/appmintNative';

export async function openStatement(show: () => void) {
  const protectedNow = await privacyScreen.enable();   // false where nothing can block capture
  if (!protectedNow) toast('Screenshots cannot be blocked here');
  show();
}

export async function closeStatement(hide: () => void) {
  hide();
  await privacyScreen.disable();
}

Catatan: Di aplikasi, blokir ini milik halaman saat ini dan dilepas saat aplikasi pindah ke rute lain, jadi aktifkan lagi saat layar itu tampil kembali. Di web can('privacyScreen') bernilai 'none' dan enable() mengembalikan false - jangan klaim ada perlindungan di sana. Tidak ada yang menghentikan kamera yang diarahkan ke layar.

appmintNative.pushInbox appmintNative#

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

Contoh

Mengembalikan pesan AppMint Push yang diterima instalasi ini (terbaru lebih dulu, hingga 50), untuk layar notifikasi dalam aplikasi.

Mengembalikan: PushMessage[] secara sinkron: { id, title, body, imageUrl, link, receivedAt, read }. Array kosong di web dan saat AppMint Push nonaktif. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat build.

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

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

Tampilkan jumlah badge belum dibaca yang mengikuti kedatangan baru:

import { useEffect, useState } from 'react';
import { pushInbox } from '@/lib/appmintNative';

export function UnreadBadge() {
  const [n, setN] = useState(() => pushInbox().filter((m) => !m.read).length);
  useEffect(() => {
    const recount = () => setN(pushInbox().filter((m) => !m.read).length);
    window.addEventListener('appmint:push', recount);
    return () => window.removeEventListener('appmint:push', recount);
  }, []);
  return n > 0 ? <span aria-label={`${n} unread`}>{n}</span> : null;
}

Render daftar, dengan keadaan kosong untuk web dan untuk build tanpa AppMint Push, dan tombol "Tandai semua sudah dibaca":

import { useState } from 'react';
import { pushInbox, markPushRead } from '@/lib/appmintNative';

export function NotificationsScreen() {
  const [items, setItems] = useState(() => pushInbox());
  if (items.length === 0) return <p>No notifications yet.</p>;
  return (
    <section>
      <button onClick={() => { markPushRead(); setItems(pushInbox()); }}>Mark all read</button>
      <ul>
        {items.map((m) => (
          <li key={m.id} style={{ fontWeight: m.read ? 400 : 700 }}>
            {m.title} <small>{new Date(m.receivedAt).toLocaleString()}</small>
            <p>{m.body}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

Catatan: kotak masuk adalah sumber kebenaran yang tersimpan - baca saat dimuat; event appmint:push hanya aktif saat aplikasi ada di layar. Pesan yang sudah ditutup tidak disertakan.

appmintNative.readBarcodeFrom appmintNative#

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

Contoh

Membaca barcode dan kode QR dari gambar yang sudah dimiliki halaman - file foto, Blob, <img>, <canvas>, atau frame video. Kamera tidak dibuka.

Mengembalikan: Promise<{ value: string; format: string }[]> - setiap kode yang ditemukan, dalam urutan detektor; array kosong jika tidak ada, jika gambar tidak bisa dibaca, atau jika pembacaan barcode tidak tersedia. Tidak pernah ditolak. Membutuhkan: tidak ada.

Baca kode dari foto yang dipilih pengguna:

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

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

Dari gambar yang sudah ada di halaman:

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

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

Baca kode dari frame canvas, dengan memeriksa dulu apa yang didukung perangkat:

import { barcodeFormats, readBarcodeFrom } from '@/lib/appmintNative';

export async function scanCanvas(canvas: HTMLCanvasElement): Promise<string | null> {
  const supported = await barcodeFormats();
  if (supported.length === 0) return null;                     // no reader on this device
  const wanted = ['qr_code', 'ean_13'].filter((f) => supported.includes(f)) as ('qr_code' | 'ean_13')[];
  const [hit] = await readBarcodeFrom(canvas, wanted);
  return hit?.value ?? null;
}

Catatan:

  • Memakai BarcodeDetector milik mesin browser (ada di WebView aplikasi Android; tidak ada di Safari dan banyak browser desktop - maka mengembalikan []).
  • <input type="file" accept="image/*"> biasa membuka pemilih foto Android dan tidak memerlukan izin penyimpanan.
  • Persempit formats jika Anda tahu formatnya: lebih cepat dan lebih jarang salah baca. barcodeFormats() mencantumkan apa yang didukung perangkat.

appmintNative.readOtp appmintNative#

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

Contoh

Menunggu kode sekali pakai SMS untuk aplikasi ini dan mengembalikannya - sheet persetujuan SMS dari Google di aplikasi Android, WebOTP di Chrome pada web.

Mengembalikan: Promise<string | null> - kodenya, atau null jika pengguna menolak, lima menit berlalu, atau signal membatalkan. Melempar error jika can('otp') bernilai 'none'. Dibutuhkan: tidak ada yang perlu diaktifkan. SMS harus diakhiri dengan @<host> #<code> (di aplikasi Android host-nya adalah appassets.androidplatform.net).

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

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

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

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

Tangani jawaban "tidak ada kode", dan tawarkan kirim ulang saat penantian berakhir tanpa kode:

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

export function VerifyStep({ resend, submit }: { resend: () => void; submit: (c: string) => void }) {
  const [waited, setWaited] = useState(false);

  async function start() {
    resend();
    if (can('otp') === 'none') return;          // readOtp would throw here — the user types it
    const code = await readOtp();               // null: declined, 5 minutes passed, or aborted
    if (code) submit(code); else setWaited(true);
  }

  return (
    <div>
      <button onClick={start}>Send code</button>
      {waited && <p>No code detected. <button onClick={start}>Send again</button></p>}
    </div>
  );
}

Tangkap error yang dilempar jika Anda tidak bisa memeriksa can('otp') lebih dulu:

import { readOtp } from '@/lib/appmintNative';

export async function tryAutoFill(): Promise<string | null> {
  try {
    return await readOtp({ signal: AbortSignal.timeout(60_000) });
  } catch {
    return null;                                // one-time codes unavailable: the user types it
  }
}

Catatan: ini hanya pemercepat - selalu biarkan kolom bisa diketik.

appmintNative.recognizeText appmintNative#

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

Contoh

Membaca teks dalam gambar di perangkat (can('ocr')): pengenal ML Kit di aplikasi Android, atau TextDetector Chrome jika diaktifkan.

Mengembalikan: Promise dari { text, blocks: [{ text, box, lines: [{ text, box }] }] }, kotak dalam piksel gambar. Menolak dengan code 'unavailable' di mana can('ocr') adalah 'none', atau DOMException saat pengenalan gagal. Membutuhkan: tidak ada, tanpa izin.

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

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

Tangani kasus "tidak ada pengenal" dan kegagalan secara eksplisit:

import { can, recognizeText } from '@/lib/appmintNative';

export async function readLabel(file: Blob): Promise<string> {
  if (can('ocr') === 'none') return '';
  try {
    const { text } = await recognizeText(file);
    return text.trim();
  } catch (e) {
    if ((e as { code?: string }).code === 'unavailable') return '';
    throw e;                                    // a real recognition failure
  }
}

Baca aksara selain Latin, dan daftarkan setiap baris beserta posisinya:

import { recognizeText } from '@/lib/appmintNative';

export async function readHindiSign(img: HTMLImageElement) {
  const { blocks } = await recognizeText(img, { script: 'devanagari' });
  return blocks.flatMap((b) => b.lines).map((l) => ({ text: l.text, box: l.box }));
}

Catatan: Hanya aksara Latin. Panggilan pertama di ponsel baru bisa memakan beberapa detik saat layanan Play mengambil modelnya - tampilkan progres.

appmintNative.remoteConfig appmintNative#

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

Contoh

Menggambar strip pengumuman dan layar pemeliharaan yang diterbitkan kreator dari Remote Update - untuk halaman saat berjalan di browser web. Di aplikasi Android, shell sudah melakukannya sendiri, sehingga panggilan ini tidak berpengaruh di sana.

Mengembalikan: start(manifestUrl?) → Promise<void> (ambil + gambar; tidak pernah ditolak); fetch(url) → Promise<RemoteConfig | null> (tidak pernah ditolak; null = tidak ada yang berlaku); apply(cfg) → fungsi pembersihan; manifestUrl() → string (content dari <meta name="appmint-remote-manifest">, '' jika tidak ada); handledHere() → boolean; dismissed(id) → boolean; dismiss(id) → void. Dibutuhkan: tidak ada di dalam aplikasi - Remote Update berjalan secara native di sana.

Aman dipanggil saat startup; di aplikasi, ini kembali seketika:

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

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

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

Atau baca nilainya dan gambar UI Anda sendiri (hanya web):

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

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

Tampilkan sendiri teks pemeliharaan di web, dan biarkan aplikasi memakai layarnya sendiri:

import { useEffect, useState } from 'react';
import { remoteConfig } from '@/lib/appmintNative';

export function useMaintenance(url: string): string | null {
  const [text, setText] = useState<string | null>(null);
  useEffect(() => {
    if (!remoteConfig.handledHere()) return;               // the Android app shows its own screen
    void remoteConfig.fetch(url).then((cfg) => {
      setText(cfg?.maintenanceMode ? cfg.maintenanceText : null);
    });
  }, [url]);
  return text;
}

Catatan:

  • RemoteConfig = { version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }.
  • handledHere() adalah false di aplikasi Android: shell membaca manifes secara native dan menggambar pengumumannya sendiri, jadi Anda tidak pernah melihat dua banner.
  • start() tanpa argumen memakai manifestUrl(). Build AppMint tidak menambahkan meta tag itu, jadi tanpa argumen (atau <meta name="appmint-remote-manifest" content="…"> milik Anda sendiri) tidak ada yang diambil.
  • fetch() mengembalikan null untuk publikasi yang dijeda atau dikembalikan, manifes yang daftar platforms-nya tidak menyertakan web, format manifes yang lebih baru, atau kesalahan jaringan apa pun.
  • Mode pemeliharaan menang: apply() lalu menutupi seluruh halaman dengan teks pemeliharaan. Pengumuman yang ditutup tetap tersembunyi sampai announcementId baru diterbitkan.

appmintNative.requestDndAccess appmintNative#

requestDndAccess() — @/lib/appmintNative

Contoh

Meminta akses Jangan Ganggu, yang diwajibkan Android sebelum aplikasi boleh mengubah volume dering atau notifikasi saat Jangan Ganggu aktif.

Mengembalikan: 'granted', 'opened' (layar pengaturan ditampilkan - periksa lagi saat pengguna kembali), 'not_declared', atau 'unavailable' di web. Dibutuhkan: tidak ada yang perlu dicentang - memanggilnya di kode Anda membuat build mendeklarasikan izin tersebut.

import { requestDndAccess, setVolume } from '@/lib/appmintNative';

export function silenceRinger(): string {
  const r = setVolume('ring', 0);
  if (r.ok) return 'done';
  if (r.error === 'dnd_access_required') return requestDndAccess();   // 'opened' → ask the user to allow it
  return r.error ?? 'failed';
}

Minta sekali, lalu coba lagi perubahan volume setelah pengguna kembali dari pengaturan:

import { useEffect, useRef } from 'react';
import { requestDndAccess, setVolume } from '@/lib/appmintNative';

export function useRingerMute() {
  const pending = useRef(false);
  useEffect(() => {
    const onShow = () => {
      if (pending.current && document.visibilityState === 'visible') {
        pending.current = false;
        setVolume('ring', 0);                       // allowed now, or the error comes back again
      }
    };
    document.addEventListener('visibilitychange', onShow);
    return () => document.removeEventListener('visibilitychange', onShow);
  }, []);

  return () => {
    if (requestDndAccess() === 'opened') pending.current = true;
  };
}

Cabangkan pada setiap jawaban yang mungkin diberikan:

import { requestDndAccess, toast } from '@/lib/appmintNative';

export function explainDnd() {
  switch (requestDndAccess()) {
    case 'granted': toast('Do Not Disturb access is already on'); break;
    case 'opened': toast('Allow this app, then come back'); break;
    case 'not_declared': toast('This build was made without that permission'); break;
    default: toast('Only available in the Android app');   // 'unavailable' on the web
  }
}

appmintNative.requestReview appmintNative#

requestReview(): Promise<boolean>

Contoh

Meminta Google Play menampilkan lembar penilaiannya sendiri di dalam aplikasi (Play In-App Review).

Mengembalikan: Promise<boolean> - true hanya jika Play menampilkan sheet; false di web, untuk pemasangan yang bukan dari Google Play, saat Play menolak (kuotanya) atau gagal. Gunakan askForReview() untuk mengetahui penyebabnya. Dibutuhkan: tidak ada; sheet hanya muncul untuk aplikasi yang dipasang dari Google Play.

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

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

Cari tahu mengapa lembar tidak tampil, tanpa mengirim pengguna pergi:

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

export async function maybeAsk() {
  const { result } = await askForReview();
  // 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable'
  if (result !== 'shown') console.info('review sheet not shown:', result);
}

Tanya hanya setelah momen sukses, dan paling banyak sebulan sekali:

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

const KEY = 'lastReviewAsk';

export async function afterSuccess() {
  const last = Number(localStorage.getItem(KEY) ?? 0);
  if (Date.now() - last < 30 * 24 * 3600 * 1000) return;
  localStorage.setItem(KEY, String(Date.now()));
  await requestReview();
}

Catatan: Google Play membatasi frekuensi sheet dan sering kali tidak menampilkan apa-apa - jangan pernah memberi hadiah untuk ulasan dan jangan buat tombol "Beri nilai" yang mengharapkan sheet terbuka (gunakan askForReview({ openStore: true }) untuk tombol). Ini tidak pernah keluar dari aplikasi ke halaman Play.

appmintNative.restorePushMessage appmintNative#

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

Contoh

Membatalkan dismissPushMessage(id) - pesan kembali ke kotak masuk AppMint Push di tempat semula.

Mengembalikan: boolean secara sinkron - true jika dipulihkan; false jika ID tidak dikenal atau tidak dibuang (selalu false di web). Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat build.

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

let lastDismissed: string | null = null;

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

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

Tawarkan Urungkan tepat setelah geser-buang, di baris React kecil:

import { useState } from 'react';
import { dismissPushMessage, restorePushMessage } from '@/lib/appmintNative';

export function SwipeRow({ id, onChange }: { id: string; onChange: () => void }) {
  const [undoable, setUndoable] = useState(false);
  return undoable ? (
    <button onClick={() => { if (restorePushMessage(id)) { setUndoable(false); onChange(); } }}>
      Undo
    </button>
  ) : (
    <button onClick={() => { if (dismissPushMessage(id)) { setUndoable(true); onChange(); } }}>
      Dismiss
    </button>
  );
}

Periksa jawabannya, karena ID yang tidak dikenal atau tidak pernah ditutup menghasilkan false:

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

export function undoOrReport(id: string): string {
  if (!restorePushMessage(id)) return 'Nothing to restore';
  return `Back in your inbox (${pushInbox().length} messages)`;
}

Catatan: pesan yang dihapus dengan clearPushInbox() atau ditarik oleh Anda tidak bisa dipulihkan.

appmintNative.safeArea appmintNative#

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

Contoh

Memberikan ruang yang diambil oleh bilah status, lubang kamera, dan bilah navigasi, dalam piksel CSS, agar bilah tetap tidak berada di bawahnya.

Mengembalikan: angka { top, right, bottom, left }, secara sinkron. Membutuhkan: tidak ada.

Biasanya CSS sudah cukup - runtime menaruh --safe-top, --safe-right, --safe-bottom, --safe-left pada :root:

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

Gunakan angkanya saat kode harus menghitung, misalnya untuk menempatkan tombol mengambang:

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

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

Jaga daftar yang digulir tetap bebas dari bilah dengan mengatur padding-nya dari angka-angka tersebut:

import { useEffect, useState, type ReactNode } from 'react';
import { safeArea } from '@/lib/appmintNative';

export function PaddedList({ children }: { children: ReactNode }) {
  const [inset, setInset] = useState({ top: 0, bottom: 0 });
  useEffect(() => {
    const read = () => { const s = safeArea(); setInset({ top: s.top, bottom: s.bottom }); };
    read();
    window.addEventListener('resize', read);
    return () => window.removeEventListener('resize', read);
  }, []);
  return <div style={{ paddingTop: inset.top + 8, paddingBottom: inset.bottom + 8 }}>{children}</div>;
}

Catatan:

  • Diukur dari CSS env(safe-area-inset-*). Nilainya di-cache dan diukur ulang setelah jendela diubah ukurannya atau diputar.
  • Sebelum document.body ada (sangat awal saat impor) ini mengembalikan nol semua - panggil dari dalam effect.
  • Di browser desktop keempatnya biasanya 0.

appmintNative.saveFile appmintNative#

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

Contoh

Menyimpan teks atau Blob sebagai file yang disimpan pengguna, dengan nama pilihan Anda. Di aplikasi membuka lembar "Simpan sebagai…" Android dengan nama itu; di browser berupa unduhan biasa.

Mengembalikan: (sinkron) boolean - true jika unduhan dimulai, false jika tidak ada document atau pembuatannya gagal. Ini tidak memberi tahu apakah pengguna menyimpan atau membatalkan sheet. Dibutuhkan: tidak ada.

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

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

Blob mempertahankan tipenya sendiri (argumen ketiga diabaikan):

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

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

Simpan hasil fetch, dan beri tahu jika tidak bisa dimulai:

import { saveFile, toast } from '@/lib/appmintNative';

export async function downloadInvoice(id: string) {
  const res = await fetch(`/api/invoices/${id}.pdf`);
  if (!res.ok) { toast('Could not get the invoice'); return; }
  const pdf = await res.blob();                          // the Blob keeps its own type
  if (!saveFile(`Invoice-${id}.pdf`, pdf)) toast('Could not start the download');
}

Catatan: Di balik layar ini adalah <a download> biasa, yang diubah aplikasi menjadi sheet "Simpan sebagai…" (lihat downloads). Selalu beri nama dengan ekstensi; tanpa itu file disimpan sebagai download. Tipe MIME default untuk string adalah text/plain.

appmintNative.saveToGallery appmintNative#

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

Contoh

Menyimpan foto atau video ke galeri ponsel - can('gallery') bernilai 'native' di aplikasi Android dan 'web' di browser (file diunduh di sana).

Mengembalikan: Promise dari URI item yang disimpan ('' di web). Menolak dengan code 'permission-denied', 'download-failed', 'bad-source' atau 'failed'. Membutuhkan: tidak ada; tanpa izin di Android 10+.

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

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

Simpan klip video dan tangani kode kegagalan yang bisa menolaknya:

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

export async function saveClip(url: string) {
  try {
    await saveToGallery(url, { kind: 'video', album: 'My Clips', fileName: 'clip.mp4' });
    toast('Saved to your Movies folder');
  } catch (e) {
    const code = (e as { code?: string }).code;
    toast(code === 'download-failed' ? 'Check your connection and try again' : 'Could not save the clip');
  }
}

Simpan gambar dari data URL, dan serahkan ke sheet berbagi hanya jika berhasil disimpan:

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

export async function saveAndShare(dataUrl: string) {
  if (can('gallery') !== 'none') await saveToGallery(dataUrl, { album: 'Receipts' });
  await share({ title: 'My receipt', text: 'Here is my receipt' });
}

Catatan: source juga bisa berupa URL data: atau URL http(s); berikan { kind: 'video' } untuk URL video tanpa tipe MIME video.

appmintNative.scanBarcode appmintNative#

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

Contoh

Membuka kamera dalam <video> yang ANDA tempatkan di tata letak, dan menyelesaikan dengan barcode atau kode QR pertama yang dilihatnya.

Mengembalikan: Promise<{ value: string; format: string } | null> - kode pertama, atau null jika dibatalkan (signal dihentikan), jika kamera ditolak, atau jika pemindaian tidak tersedia. Tidak pernah ditolak dan selalu menghentikan kamera. Membutuhkan: akses kamera - di aplikasi Android aktifkan Camera di Langkah 4 (Akses) saat Anda membuat build.

Gunakan hanya jika jendela bidik harus berada di dalam layar Anda sendiri; jika tidak, scanCode() lebih sederhana.

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

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

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

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

Gunakan kamera depan:

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

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

Tutup pemindai setelah hasil pertama dan tawarkan kode ketikan saat kamera tidak tersedia:

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

export function TicketCheck({ onCode }: { onCode: (v: string) => void }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const ctl = useRef<AbortController | null>(null);
  const [typed, setTyped] = useState(false);

  async function start() {
    if (!videoRef.current || can('barcodeScan') === 'none') { setTyped(true); return; }
    ctl.current = new AbortController();
    const hit = await scanBarcode(videoRef.current, { signal: ctl.current.signal, formats: ['ean_13', 'code_128'] });
    if (hit) onCode(hit.value);
    else if (!ctl.current.signal.aborted) setTyped(true);   // null and not cancelled: no camera
  }

  return (
    <div>
      <video ref={videoRef} muted playsInline />
      <button onClick={start}>Scan ticket</button>
      <button onClick={() => ctl.current?.abort()}>Cancel</button>
      {typed && <input placeholder="Type the ticket number" onBlur={(e) => onCode(e.target.value)} />}
    </div>
  );
}

Catatan:

  • Ini memanggil getUserMedia (sehingga pengguna melihat prompt izin kamera) dan mendekode dengan BarcodeDetector pada setiap frame. can('barcodeScan') bernilai 'none' jika salah satunya tidak ada.
  • Build memindai kode Anda: panggilan scanBarcode( mengaktifkan Kamera untuk Anda. Jika Anda mematikannya secara manual, permintaan kamera gagal dan Anda mendapatkan null.
  • Mulai dari tindakan pengguna (tombol Pindai atau membuka layar pemindaian), jangan pernah saat aplikasi diluncurkan. Tangani null sebagai hasil normal dengan opsi input manual.

appmintNative.scanCode appmintNative#

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

Contoh

Membuka pemindai layar penuh dan selesai dengan barcode atau kode QR pertama - satu panggilan, tidak ada yang perlu digambar.

Mengembalikan: Promise<ScanCodeResult | null> - { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }, atau null jika pengguna menutup pemindai. Selain itu ditolak dengan Error yang code-nya adalah 'unavailable' (tidak ada pemindai di sini - Play services tidak ada di aplikasi; tidak ada BarcodeDetector atau kamera di web), 'busy' (pemindaian sudah terbuka), 'camera-denied' (web: izin kamera ditolak) atau 'failed'. Dibutuhkan: tidak ada di aplikasi (pemindai Google tidak memerlukan izin kamera).

Dari tombol Pindai:

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

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

Gunakan bagian yang sudah diurai yang diisi aplikasi (Wi-Fi, tautan, kontak …), dengan cadangan ketikan manual di pemindai Google:

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

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

Tunda dari tombol di layar alarm Anda sendiri, memakai onAlarm untuk mengetahui kapan alarm berbunyi.

import { useEffect, useState } from 'react';
import { onAlarm, snoozeAlarm, stopAlarm } from '@/lib/appmintNative';

export function RingingScreen() {
  const [ringing, setRinging] = useState(false);
  useEffect(() => onAlarm((e) => setRinging(e.action === 'open' && e.ringing === true)), []);
  if (!ringing) return null;
  return (
    <div>
      <button onClick={() => { if (snoozeAlarm('', 10)) setRinging(false); }}>Snooze 10 min</button>
      <button onClick={() => { stopAlarm(); setRinging(false); }}>Stop</button>
    </div>
  );
}

snoozeAlarm menjawab false jika tidak ada yang berdering, jadi periksa sebelum menjanjikan dering kedua kepada pengguna.

import { snoozeAlarm } from '@/lib/appmintNative';

export function snoozeOrExplain(minutes = 0) {
  return snoozeAlarm('', minutes)
    ? `Ringing again in ${minutes || 'a few'} minutes`
    : 'No alarm is ringing right now.';
}

Catatan:

  • Di aplikasi ini adalah pemindai kode milik Google (layanan Play, melalui AppMint.scanCode milik shell): zoom otomatis, tanpa permintaan kamera. type adalah 'url', 'wifi', 'text', 'phone', 'email', 'sms', 'geo', 'contact', 'calendar', 'isbn', 'product' atau 'driver_license', dan kolom yang sesuai terisi. manualInput memungkinkan pengguna mengetik kodenya.
  • Di web ini menggambar lapisan kamera layar penuh sendiri (tombol tutup, gestur kembali menutupnya) dan mendekode dengan BarcodeDetector; hanya value dan format yang terisi di sana, dan browser meminta izin kamera.
  • null selalu berarti pengguna menutup pemindai; setiap hasil lain menolak dengan code, jadi tampilkan pesan di catch.
  • Panggil dari ketukan, jangan saat halaman dimuat. Persempit formats jika Anda mengetahuinya.

appmintNative.scheduleAlarm appmintNative#

scheduleAlarm(id, input) — @/lib/appmintNative

Contoh

Mengatur alarm sungguhan dari aplikasi buatan AI: berdering pada volume alarm ponsel (menembus mode senyap, dan menembus Jangan Ganggu jika pengguna mengizinkan alarm), terus berdering sampai Tutup / Tunda atau ringSeconds, dan menampilkan aplikasi di atas layar kunci.

Mengembalikan: boolean, secara sinkron - true jika berhasil diatur; false di web, di iOS, atau jika alarmSupport() bukan 'ready'. ID yang sama menggantikan alarm lama. Membutuhkan: 🔔 Alarm clock di langkah Akses (Langkah 4) saat Anda membuat build.

import { alarmSupport, scheduleAlarm } from '@/lib/appmintNative';

export function setWeekdayAlarm(hour: number, minute: number): string {
  const support = alarmSupport();
  if (support !== 'ready') return support;       // show the matching fix-it step

  const first = new Date();
  first.setHours(hour, minute, 0, 0);
  if (first.getTime() <= Date.now()) first.setDate(first.getDate() + 1);

  const ok = scheduleAlarm('wake', {
    at: first.getTime(),
    title: 'Wake up',
    repeat: 'weekdays',
    days: [1, 2, 3, 4, 5],     // ISO: Monday = 1 … Sunday = 7
    snoozeMinutes: 9,
    ringSeconds: 300,
  });
  return ok ? 'set' : 'failed';
}

Tawarkan alarm hanya saat aplikasi bisa membunyikannya, dan jelaskan apa yang kurang jika tidak:

import { alarmSupport, scheduleAlarm } from '@/lib/appmintNative';

const HELP: Record<string, string> = {
  not_enabled: 'Alarm clock is switched off in this build.',
  needs_exact_alarm: 'Allow "Alarms & reminders" for this app in Settings.',
  needs_notification_permission: 'Allow notifications for this app in Settings.',
  unavailable: 'Alarms work only in the Android app.',
};

export function setTimer(minutes: number): string {
  const s = alarmSupport();
  if (s !== 'ready') return HELP[s] ?? 'Alarm not available';
  const ok = scheduleAlarm('timer', {
    at: Date.now() + minutes * 60_000,
    title: 'Timer done',
    snoozeMinutes: 0,          // hides the Snooze button
    ringSeconds: 60,
  });
  return ok ? `Timer set for ${minutes} min` : 'Could not set the timer';
}

Tanggapi cara alarm yang berdering berakhir, dan hentikan dari layar Anda sendiri:

import { useEffect, useState } from 'react';
import { onAlarm, stopAlarm, snoozeAlarm, isAlarmRinging } from '@/lib/appmintNative';

export function useAlarmScreen() {
  const [ringing, setRinging] = useState(isAlarmRinging());
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open') setRinging(!!e.ringing);
    else if (e.action === 'dismiss' || e.action === 'snooze' || e.action === 'missed') setRinging(false);
    else if (e.action === 'error') console.warn('Alarm error', e.error);
  }), []);
  return {
    ringing,
    stop: () => stopAlarm(),            // '' = whichever alarm is ringing
    snooze: () => snoozeAlarm('', 5),   // ring again in 5 minutes
  };
}

Catatan: Hanya untuk aplikasi Android. repeat harian / mingguan / hari kerja / bulanan / tahunan mempertahankan jam yang sama saat terjadi perubahan waktu musim panas. Batalkan dengan cancelNotification(id) pada bridge; hasilnya datang melalui onAlarm.

appmintNative.ScreenOrientation appmintNative#

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

Contoh

Mengunci layar ke potret atau lanskap untuk satu tampilan, dalam bentuk plugin ScreenOrientation Capacitor - kode yang sama dengan lockOrientation().

Mengembalikan: Promise<void> untuk keduanya. lock menolak, seperti milik Capacitor, saat orientation bukan tipe kunci atau platform menolak kuncinya. Membutuhkan: tidak ada.

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

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

Kunci untuk satu layar saja hanya saat kunci diizinkan, dan biarkan pengguna memutar kembali:

import { useEffect, useState } from 'react';
import { ScreenOrientation } from '@/lib/appmintNative';

export function ChartView() {
  const [locked, setLocked] = useState(true);
  useEffect(() => {
    if (locked) void ScreenOrientation.lock({ orientation: 'landscape-primary' });   // any 'landscape*' value
    else void ScreenOrientation.unlock();
    return () => { void ScreenOrientation.unlock(); };
  }, [locked]);
  return <button onClick={() => setLocked((v) => !v)}>{locked ? 'Free rotation' : 'Lock landscape'}</button>;
}

Jika Anda perlu tahu apakah kunci berhasil, gunakan lockOrientation() secara langsung (menjawab false di tempat penguncian ditolak, seperti browser desktop):

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

export async function openSignaturePad() {
  const ok = await lockOrientation('landscape');
  if (!ok) toast('Turn your phone sideways for more room');
}

Catatan: orientation adalah salah satu jenis kunci Capacitor - 'any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'portrait-secondary', 'landscape-primary', 'landscape-secondary' - dan diterapkan apa adanya. Browser desktop menolak kunci; browser ponsel biasanya hanya mengizinkannya dalam layar penuh. lockOrientation(to) mengembalikan true/false. Beri pengguna jalan keluar dari tampilan yang terkunci.

appmintNative.screenReader appmintNative#

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

Contoh

Memberi tahu aplikasi apakah TalkBack menyala, dan mengumumkan pesan singkat.

Mengembalikan: isEnabled() → Promise<boolean> (false di web - tidak ada API web). speak() → Promise<void>. onChange(cb) → fungsi berhenti berlangganan; cb(enabled) berjalan saat pembaca layar dinyalakan atau dimatikan (hanya di aplikasi). Dibutuhkan: tidak ada.

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

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

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

Umumkan hasil hanya kepada pengguna pembaca layar, dan biarkan toast visual untuk yang lain:

import { screenReader, toast } from '@/lib/appmintNative';

export async function notifySaved() {
  if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
  else toast('Note saved');
}

Hapus animasi saat TalkBack aktif, dan berhenti mendengarkan saat komponen hilang:

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

export function Banner({ text }: { text: string }) {
  const [reduce, setReduce] = useState(false);
  useEffect(() => {
    let alive = true;
    void screenReader.isEnabled().then((v) => { if (alive) setReduce(v); });
    const off = screenReader.onChange(setReduce);   // returns an unsubscribe function
    return () => { alive = false; off(); };
  }, []);
  return <div className={reduce ? '' : 'animate-pulse'} role="status" aria-live="polite">{text}</div>;
}

Catatan: Di aplikasi speak berjalan melalui plugin ScreenReader; di web (atau jika plugin gagal) diucapkan dengan text-to-speech. Utamakan markup yang baik (aria-label, wilayah aria-live) untuk sebagian besar pengumuman; gunakan speak untuk perubahan singkat yang penting.

appmintNative.searchContacts appmintNative#

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

Contoh

Menemukan kontak yang cocok dengan kata pencarian. Hanya aplikasi.

Mengembalikan: Promise<Contact[]> - kosong jika tidak ada yang cocok, akses ditolak, atau Kontak nonaktif. Membutuhkan: sama seperti listContacts: Kontak di Langkah 4 (Akses) (diaktifkan oleh build saat kode Anda memanggil ini), dan izin pengguna saat pertama kali dipakai.

Kotak pencarian dengan jeda singkat saat mengetik.

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

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

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

Tampilkan status yang jelas saat Kontak tidak tersedia dan saat tidak ada yang cocok:

import { useState } from 'react';
import { can, searchContacts, type Contact } from '@/lib/appmintNative';

export function ContactPicker({ onPick }: { onPick: (c: Contact) => void }) {
  const [rows, setRows] = useState<Contact[] | null>(null);
  if (can('contactList') === 'none') return <p>Choosing a contact works only in the Android app.</p>;
  return (
    <div>
      <input placeholder="Search contacts" onChange={async (e) => setRows(await searchContacts(e.target.value, 10))} />
      {rows && rows.length === 0 && <p>No contacts match, or access was not allowed.</p>}
      {rows?.map((c) => <button key={c.id ?? c.name} onClick={() => onPick(c)}>{c.name} {c.phones?.[0]}</button>)}
    </div>
  );
}

Cari seseorang berdasarkan nama, lalu isi otomatis pesan teks kepadanya:

import { searchContacts, composeSms } from '@/lib/appmintNative';

export async function textPerson(name: string, message: string): Promise<boolean> {
  const [first] = await searchContacts(name, 1);
  const number = first?.phones?.[0];
  return number ? composeSms(number, message) : false;   // the user presses send
}

Catatan: Pencocokan dilakukan oleh pencarian kontak ponsel pada nama. Setiap kontak adalah { id?, name?, phones?: string[], emails?: string[], photoUri? }. Di web can('contactList') bernilai 'none' - tidak ada web API untuk membaca buku alamat. Kontak adalah data pribadi: deklarasikan di formulir Data safety Play dan kebijakan privasi Anda.

appmintNative.SecureKeyError appmintNative#

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

Contoh

Kesalahan yang dilempar secureKeys. code-nya adalah satu kata pendek yang bisa Anda cabangkan, sama di aplikasi dan di web.

Mengembalikan: sebuah Error dengan name === 'SecureKeyError' dan code: 'no-such-key', 'auth-required' (buka kunci dan coba lagi), 'key-invalidated' (biometrik atau kunci layar berubah - datanya hilang), 'strongbox-unavailable', 'disabled' (Secure Keys tidak ada di build ini / tidak ada Web Crypto), 'bad-alias', 'bad-payload' (bukan base64 yang valid atau terlalu pendek), 'too-large' (lebih dari 4 MB dalam satu panggilan di Android), 'foreign-origin' (halaman dari situs lain yang meminta), 'failed' (lainnya). Membutuhkan: tidak ada.

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

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

Ubah kode menjadi pesan singkat untuk pengguna di satu tempat:

import { SecureKeyError } from '@/lib/appmintNative';

export function secureKeyMessage(e: unknown): string {
  if (!(e instanceof SecureKeyError)) return 'Something went wrong';
  switch (e.code) {
    case 'auth-required': return 'Unlock your phone to continue';
    case 'key-invalidated': return 'Your screen lock changed, so this data was erased';
    case 'too-large': return 'That is too big to protect in one piece';
    case 'foreign-origin': return 'This page is not allowed to use secure storage';
    default: return 'Secure storage failed (' + e.code + ')';
  }
}

Perlakukan payload yang diutak-atik atau salah sebagai hasil biasa dan hapus, tetapi tampilkan kunci yang hilang sebagai error:

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

export async function readNote(sealed: string): Promise<string | null> {
  try {
    return await secureKeys.decrypt('notes', sealed);
  } catch (e) {
    if (e instanceof SecureKeyError && (e.code === 'bad-payload' || e.code === 'failed')) {
      localStorage.removeItem('note');   // the stored text cannot be read any more
      return null;
    }
    throw e;
  }
}

Catatan: e.message adalah code atau "code: detail"; bercabanglah berdasarkan e.code, jangan pernah berdasarkan pesannya. Kata kesalahan bawaan platform diubah menjadi kode-kode ini, dan teks aslinya disimpan di dalam pesan. Payload yang berubah atau asing adalah 'bad-payload' di web dan 'failed' di aplikasi, yang tidak bisa membedakannya dari kesalahan cipher lainnya.

appmintNative.secureKeys appmintNative#

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

Contoh

Kunci enkripsi yang dipegang perangkat dan tidak pernah bisa diekspor aplikasi: Android Keystore di aplikasi, dan kunci Web Crypto yang tidak bisa diekstrak di web. Menghapus kunci adalah satu-satunya "hapus aman" yang sebenarnya.

Mengembalikan: semuanya Promise. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked}; encrypt(alias, text) → string base64; decrypt(alias, base64) → teksnya; remove(alias) / has(alias) / isHardwareBacked(alias) → boolean; list() → string[]. generate, encrypt dan decrypt melempar SecureKeyError dengan code. Membutuhkan: Secure Keys (encryption) di Langkah 4 (Akses) saat Anda membuat build.

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

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

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

Hapus sungguhan - tidak ada yang bisa membaca data tersegel setelah ini:

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

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

Daftar kunci yang dimiliki aplikasi dan tampilkan apakah masing-masing dilindungi perangkat keras (web selalu menjawab false):

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

export async function describeKeys(): Promise<string[]> {
  if (can('secureKeys') === 'none') return [];
  const aliases = await secureKeys.list();
  return Promise.all(aliases.map(async (alias) =>
    `${alias}: ${(await secureKeys.isHardwareBacked(alias)) ? 'secure hardware' : 'software'}`));
}

Catatan: can('secureKeys') adalah 'native' di aplikasi dan 'web' di browser, di mana isHardwareBacked() selalu false - jangan pernah menyebut "hardware" di sana. Kunci requireAuth: di aplikasi, encrypt/decrypt melempar auth-required tanpa menampilkan apa pun setelah jendela buka kunci berlalu - panggil unlock() dan coba lagi; di web, runtime menampilkan unlock() sendiri. Enkripsi satu catatan sekali waktu (aplikasi membatasi satu panggilan hingga 4 MB). Alias: huruf, angka, _ - ., maks 64.

appmintNative.sendSms appmintNative#

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

Contoh

Mengirim SMS langsung, tanpa layar konfirmasi. Hanya aplikasi Android.

Mengembalikan: Promise<boolean> - true jika ponsel melaporkan pesan terkirim, false jika gagal, jika SMS tidak diaktifkan, atau jika pengguna menolak izin. Membutuhkan: sakelar SMS (read & send) di Langkah 4 (Akses) - build mengaktifkannya saat kode Anda memanggil ini - dan izin pengguna saat pertama kali dipakai.

Kirim peringatan dan beri tahu pengguna apa yang terjadi.

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

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

Minta konfirmasi lebih dulu, karena pesan terkirim tanpa layar untuk membatalkannya:

import { sendSms, composeSms, can, toast } from '@/lib/appmintNative';

export async function sendCheckIn(to: string) {
  if (can('smsSend') === 'none') { composeSms(to, 'I am safe'); return; }   // the user presses send
  if (!window.confirm('Send "I am safe" to ' + to + '? This may cost money.')) return;
  toast((await sendSms(to, 'I am safe')) ? 'Sent' : 'Could not send. Check the SMS permission.');
}

Kirim peringatan yang sama ke beberapa orang, satu per satu, dan hitung berapa yang terkirim:

import { sendSms } from '@/lib/appmintNative';

export async function alertAll(numbers: string[], text: string) {
  let sent = 0;
  for (const n of numbers) {
    if (await sendSms(n, text)) sent += 1;      // waits for the phone's answer for each one
  }
  return { sent, failed: numbers.length - sent };
}

Catatan: Wrapper selesai pada jawaban PERTAMA, yaitu sent (pesan sudah keluar dari ponsel), bukan delivered. Untuk laporan pengiriman gunakan WebToApk.sendSms mentah dengan event appmint:sms. can('smsSend') bernilai 'none' di web (tidak ada browser yang bisa mengirim SMS). Setiap SMS mungkin membebani pulsa pengguna. SMS adalah izin terbatas Google Play: Anda memerlukan Permissions Declaration yang disetujui di Play Console, jadi pilih composeSms() jika pengguna bisa menekan kirim sendiri.

appmintNative.setInAppPresentation appmintNative#

import { setInAppPresentation } from '@/lib/appmintNative'

Contoh

Memilih siapa yang menggambar pesan dalam aplikasi (kampanye pembuat): aplikasi ('auto', default: desain banner, kartu, sheet, atau layar penuh dari pembuat) atau UI Anda sendiri ('none', melalui onInAppMessage).

Mengembalikan: boolean secara sinkron: true jika tersimpan (tetap ada di ponsel). Selalu false di web dan tanpa AppMint Push. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi.

import { setInAppPresentation } from '@/lib/appmintNative';

// The app draws its own messages from now on.
setInAppPresentation('none');

Sakelar pengaturan:

import { setInAppPresentation } from '@/lib/appmintNative';

export function onNativePopupsChange(on: boolean) {
  if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}

Ambil alih saat satu layar terbuka, lalu kembalikan:

import { useEffect } from 'react';
import { setInAppPresentation } from '@/lib/appmintNative';

export function useQuietScreen() {
  useEffect(() => {
    setInAppPresentation('none');
    return () => { setInAppPresentation('auto'); };
  }, []);
}

Catatan: pesan yang diserahkan ke kode Anda dihitung sebagai sudah ditampilkan. Mengatur 'none' juga menutup pesan yang sedang tampil di layar.

appmintNative.setMusicVolume appmintNative#

setMusicVolume(percent) — @/lib/appmintNative

Contoh

Mengatur level musik latar aplikasi yang berulang, 0 sampai 100 (dibulatkan dan dibatasi otomatis untuk Anda).

Mengembalikan: void. Dibutuhkan: file yang dipilih di bawah Background Music pada Langkah 3 (Integrate) saat Anda membuat aplikasi. Tidak melakukan apa-apa jika musicVolume() bernilai -1.

Aplikasi menyimpan levelnya untuk peluncuran berikutnya, jadi tombol bisukan sudah cukup:

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

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

Penggeser volume yang hanya muncul saat aplikasi menyertakan musik:

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

export function MusicSlider() {
  const [level, setLevel] = useState(musicVolume());
  if (level < 0) return null;                       // no music in this app: draw no control
  return (
    <input type="range" min={0} max={100} value={level}
      onChange={(e) => { const v = Number(e.target.value); setLevel(v); setMusicVolume(v); }} />
  );
}

Pelankan musik saat video diputar dan kembalikan sesudahnya:

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

let before = -1;
export function duckMusic() { before = musicVolume(); if (before > 0) setMusicVolume(10); }
export function restoreMusic() { if (before >= 0) setMusicVolume(before); before = -1; }

Catatan: 0 membuat musik senyap tetapi tetap diputar. Levelnya disimpan di ponsel (sampai pengguna menghapus data aplikasi). Hanya aplikasi Android; di web tidak terjadi apa-apa.

appmintNative.setPushPresentation appmintNative#

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

Contoh

Memilih siapa yang menggambar pesan AppMint Push yang Anda kirim dengan gaya dalam aplikasi (banner, pop-up, bottom sheet, atau layar penuh) saat aplikasi terbuka: shell aplikasi ('auto', bawaan) atau kode Anda sendiri ('none').

Mengembalikan: boolean secara sinkron - true jika mode tersimpan. Selalu false di web. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi.

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

// Option A — keep the shell's banner, and never draw a second one on top of it.
export function usePushBanner(show: (title: string) => void) {
  useEffect(() => {
    const onPush = (e: Event) => {
      const d = (e as CustomEvent).detail;
      // { type: 'received', message, presented } — presented = the shell already showed it
      if (d.type === 'received' && !d.presented) show(d.message.title);
    };
    window.addEventListener('appmint:push', onPush);
    return () => window.removeEventListener('appmint:push', onPush);
  }, [show]);
}

// Option B — the app draws every message itself.
export function takeOverPushBanners() {
  setPushPresentation('none');   // saved on the phone; 'auto' gives it back to the shell
  return pushInbox();
}

Periksa jawabannya, karena nilainya false di web atau saat AppMint Push nonaktif:

import { setPushPresentation } from '@/lib/appmintNative';

export function useOwnBanners(on: boolean): boolean {
  const saved = setPushPresentation(on ? 'none' : 'auto');
  if (!saved) console.info('Push banners are not controllable here');
  return saved;
}

Gambar banner Anda sendiri untuk setiap pesan dan jaga kotak masuk tetap sinkron:

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

export function useCustomBanner() {
  const [msg, setMsg] = useState<PushMessage | null>(null);
  useEffect(() => {
    setPushPresentation('none');                  // the shell draws nothing; the page does
    const onPush = (e: Event) => {
      const d = (e as CustomEvent).detail;
      if (d.type === 'received') setMsg(d.message);
      if (d.type === 'revoked') setMsg((m) => (m?.id === d.id ? null : m));
    };
    window.addEventListener('appmint:push', onPush);
    return () => { window.removeEventListener('appmint:push', onPush); setPushPresentation('auto'); };
  }, []);
  return { msg, unread: pushInbox().filter((m) => !m.read).length, dismiss: () => { if (msg) markPushRead(msg.id); setMsg(null); } };
}

Catatan: kotak masuk dan event appmint:push bekerja sama di kedua mode. Mengatur 'none' juga menutup pesan yang sedang ditampilkan shell saat ini.

appmintNative.setPushTag appmintNative#

import { setPushTag } from '@/lib/appmintNative'

Contoh

Mengatur satu tag AppMint Push pada instalasi ini. Pusat pengiriman bisa mengirim ke semua yang punya tag, dan otomatisasi tag bisa mengirim pesan pada waktu tertentu setelah tag diatur. '' menghapus tag.

Mengembalikan: boolean secara sinkron - true jika tersimpan; false jika kuncinya bukan 1-32 huruf, angka, atau _, nilainya lebih dari 128 karakter, atau pemasangan akan melewati 50 tag. Selalu false di web. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi; penargetan tag dan otomatisasi menjangkau aplikasi yang dibuat mulai 30 Sep 2026.

import { useEffect } from 'react';
import { setPushTag } from '@/lib/appmintNative';

// Abandoned cart: a tag automation on "cart" (say 60 minutes) reminds only phones whose cart is
// still open when the time is up. Setting 'open' again does not restart the clock.
export function useCartPushTag(itemCount: number) {
  const hasItems = itemCount > 0;
  useEffect(() => {
    setPushTag('cart', hasItems ? 'open' : '');
  }, [hasItems]);
}

export function onCheckoutComplete() {
  setPushTag('cart', '');
}

Periksa hasilnya: false berarti tag ditolak (kunci tidak valid, nilai lebih dari 128 karakter, lebih dari 50 tag) atau ini adalah web, jadi simpan sendiri catatan statusnya.

import { setPushTag } from '@/lib/appmintNative';

export function markNewsletterChoice(subscribed: boolean) {
  const saved = setPushTag('newsletter', subscribed ? 'yes' : '');
  if (!saved) console.warn('Push tag not saved (web, Push off, or invalid key/value)');
  return saved;
}

Beri tag pada ponsel sekali saat mencapai pencapaian, agar pusat pengiriman bisa menargetkan semua orang di level itu.

import { useEffect } from 'react';
import { setPushTag } from '@/lib/appmintNative';

export function useLevelTag(level: number) {
  useEffect(() => {
    if (level >= 10) setPushTag('veteran', 'yes');
    setPushTag('level', String(level));
  }, [level]);
}

Catatan: tag milik pemasangan, bukan milik pengguna yang masuk. Jangan pernah menaruh data pribadi di dalam tag.

appmintNative.setPushTags appmintNative#

import { setPushTags } from '@/lib/appmintNative'

Contoh

Mengatur beberapa tag AppMint Push dalam satu perubahan; nilai '' menghapus tag itu, tag yang tidak Anda sebutkan dipertahankan.

Mengembalikan: boolean secara sinkron - true jika tersimpan; false jika ada kunci atau nilai yang melanggar aturan setPushTag (maka tidak ada yang disimpan). Selalu false di web. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrasi (Langkah 3) saat Anda membuat build; penargetan tag dan otomatisasi menjangkau aplikasi yang dibuat mulai 30 Sep 2026.

import { setPushTags, getPushTags, setPushUser } from '@/lib/appmintNative';

export function tagProfile(profile: { plan: 'free' | 'pro'; level: number; team?: string }) {
  return setPushTags({
    plan: profile.plan,
    level: String(profile.level),   // values are text
    team: profile.team ?? '',       // '' removes the tag
  });
}

// Sign-out: clear every tag and the user id.
export function signOutOfPush() {
  const cleared = Object.fromEntries(Object.keys(getPushTags()).map((k) => [k, '']));
  setPushTags(cleared);
  setPushUser('');
}

Beri tag pada ponsel dengan bahasa dan topik pilihannya saat pengguna menyimpan pengaturan, dan periksa apakah setiap tag diterima.

import { setPushTags } from '@/lib/appmintNative';

export function savePreferences(prefs: { lang: string; topics: string[] }) {
  const ok = setPushTags({
    lang: prefs.lang,
    topics: prefs.topics.join(',').slice(0, 128),   // a value is at most 128 characters
    onboarded: 'yes',
  });
  if (!ok) console.warn('Push tags not saved: nothing was changed');
  return ok;
}

Jaga agar tag tetap sinkron dengan akun yang masuk di effect React, hapus yang tidak berlaku lagi.

import { useEffect } from 'react';
import { setPushTags, setPushUser } from '@/lib/appmintNative';

export function usePushProfile(user: { id: string; plan: 'free' | 'pro'; trialEnds?: string } | null) {
  useEffect(() => {
    if (!user) return;
    setPushUser(user.id);
    setPushTags({ plan: user.plan, trial: user.plan === 'pro' ? '' : (user.trialEnds ?? '') });
  }, [user?.id, user?.plan, user?.trialEnds]);
}

Catatan: semua dari satu panggilan disimpan atau tidak sama sekali.

appmintNative.setPushUser appmintNative#

import { setPushUser } from '@/lib/appmintNative'

Contoh

Memberi tahu AppMint Push pengguna Anda yang mana yang masuk di instalasi ini, agar pusat pengiriman bisa mengirim pesan ke pengguna itu. Berikan ID akun Anda yang stabil setelah masuk dan '' saat keluar.

Mengembalikan: boolean secara sinkron - true jika id tersimpan; false untuk id yang berisi selain huruf, angka, dan _ @ . : + - (hingga 128). Selalu false di web. Dibutuhkan: Android: AppMint Push diaktifkan di langkah Integrate (Langkah 3) saat Anda membuat aplikasi; pengiriman ke pengguna menjangkau aplikasi yang dibuat mulai 30 Sep 2026.

import { useEffect } from 'react';
import { setPushUser, getPushUser } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';

// Keeps AppMint Push in step with the signed-in account: the auth user id, never the email.
export function usePushUser() {
  useEffect(() => {
    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const id = session?.user.id ?? '';
      if (getPushUser() !== id) setPushUser(id);
    });
    return () => data.subscription.unsubscribe();
  }, []);
}

Tangani ID yang ditolak: setPushUser menjawab false untuk alamat email atau ID apa pun dengan karakter di luar himpunan yang diizinkan, jadi kirim ID yang stabil sebagai gantinya.

import { setPushUser } from '@/lib/appmintNative';

export function linkPushToAccount(account: { id: string; email: string }) {
  const ok = setPushUser(account.id);   // the id, never the email
  if (!ok) console.warn('Push user id not saved (web, Push off, or invalid characters)');
  return ok;
}

Hapus ID saat pengguna keluar akun, dalam penangan klik biasa.

import { setPushUser, getPushUser } from '@/lib/appmintNative';

export function SignOutButton({ signOut }: { signOut: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await signOut();
        if (getPushUser()) setPushUser('');   // this install stops receiving "send to this user" messages
      }}
    >
      Sign out
    </button>
  );
}

Gabungkan dengan tag agar satu langkah masuk mengidentifikasi pengguna dan paketnya sekaligus.

import { setPushUser, setPushTag } from '@/lib/appmintNative';

export function onSignedIn(userId: string, plan: 'free' | 'pro') {
  setPushUser(userId);
  setPushTag('plan', plan);
}

Catatan: ID itu milik instalasi, jadi keluar akun harus menghapusnya. Ini tidak menghapus tag - panggil setPushTags dengan nilai '' untuk tag yang spesifik pengguna.

appmintNative.setStatusBar appmintNative#

setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void

Contoh

Mewarnai bilah status sistem agar sesuai dengan layar, dan memilih ikon terang atau gelap untuknya.

Mengembalikan: tidak ada. Membutuhkan: tidak ada.

style adalah warna IKON: 'light' = ikon terang untuk bilah gelap (bawaan), 'dark' = ikon gelap untuk bilah terang. Panggil sekali saat startup dengan --bg aplikasi, dan lagi di layar mana pun dengan latar belakang berbeda:

import { useEffect } from 'react';
import { setStatusBar } from '@/lib/appmintNative';

export function App() {
  useEffect(() => { setStatusBar('#0B0F1A', 'light'); }, []);   // dark app, light icons
  return <main>{/* … */}</main>;
}

export function PhotoViewer() {
  useEffect(() => {
    setStatusBar('#000000', 'light');
    return () => setStatusBar('#0B0F1A', 'light');                // restore on leave
  }, []);
  return null;
}

Layar terang:

import { setStatusBar } from '@/lib/appmintNative';

setStatusBar('#FFFFFF', 'dark');

Bentuk ala Capacitor (style dinamai menurut LATAR BELAKANG: 'DARK' = ikon terang):

import { StatusBar } from '@/lib/appmintNative';

await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });

Catatan:

  • Di aplikasi Android, shell mewarnai bilah sendiri, yang juga berfungsi di Android 15 edge-to-edge; warna yang tidak bisa diurai diabaikan - berikan #RRGGBB. Panggilan khusus gaya (backgroundColor = null) mempertahankan warna yang terakhir diatur halaman ini dan membalik ikonnya; sebelum halaman mengatur warna apa pun, ia membalik ikon lewat plugin StatusBar dan bilah mempertahankan warna yang dipilih saat build.
  • Di web, ini mengatur <meta name="theme-color">; browser memilih warna ikon sendiri.

appmintNative.setVolume appmintNative#

setVolume(stream, percent, showUi?) — @/lib/appmintNative

Contoh

Mengatur level volume ponsel itu sendiri untuk satu aliran suara, 0-100. showUi menampilkan penggeser volume ponsel saat bergerak.

Mengembalikan: { ok, stream?, current?, max?, percent?, error? } - error adalah unknown_stream, bad_percent, fixed_volume, dnd_access_required (Do Not Disturb memblokir dering / notifikasi - panggil requestDndAccess()), muted ("Total silence" Do Not Disturb membisukan stream, jadi levelnya tidak berubah), not_applied, foreign_origin, failed, atau unavailable di web. Membutuhkan: tidak ada untuk media, alarm, dan panggilan.

import { useState } from 'react';
import { getVolume, setVolume, requestDndAccess } from '@/lib/appmintNative';

export function RingVolume() {
  const [level, setLevel] = useState(() => getVolume('ring')?.percent ?? null);
  if (level === null) return null;                   // web: no phone volume to set
  return (
    <input
      type="range" min={0} max={100} value={level}
      onChange={(e) => {
        const r = setVolume('ring', Number(e.target.value));
        if (r.ok) setLevel(r.percent ?? level);
        else if (r.error === 'dnd_access_required') requestDndAccess();
      }}
    />
  );
}

Tampilkan volume media sebagai slider yang mengikuti tombol fisik, dengan memakai onVolumeChange.

import { useEffect, useState } from 'react';
import { getVolume, setVolume, onVolumeChange } from '@/lib/appmintNative';

export function MediaVolume() {
  const [pct, setPct] = useState<number | null>(() => getVolume('media')?.percent ?? null);
  useEffect(() => onVolumeChange((l) => { if (l.stream === 'media') setPct(l.percent); }), []);
  if (pct === null) return null;   // web: no phone volume
  return <input type="range" min={0} max={100} value={pct} onChange={(e) => setVolume('media', Number(e.target.value))} />;
}

Naikkan stream alarm sebelum layar alarm berbunyi, dan laporkan mengapa levelnya tidak berubah.

import { setVolume } from '@/lib/appmintNative';

export function louderAlarm() {
  const r = setVolume('alarm', 90, true);   // true shows the phone's volume slider
  if (!r.ok) {
    switch (r.error) {
      case 'fixed_volume': return 'This phone has a fixed volume.';
      case 'unavailable': return 'Volume control needs the Android app.';
      default: return `Could not change the volume (${r.error}).`;
    }
  }
  return `Alarm volume is now ${r.percent}%`;
}

appmintNative.share appmintNative#

share(input: { title?, text?, url?, files? }): Promise<boolean>

Contoh

Membuka sheet berbagi sistem di aplikasi, dan Web Share API di browser.

Mengembalikan: Promise<boolean> - true jika sheet terbuka; false jika pengguna menutupnya atau berbagi tidak tersedia. Ditolak jika isi berbagi tidak valid (misalnya url yang tidak didukung) atau ada berbagi lain yang masih terbuka. Dibutuhkan: tidak ada.

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

export function ShareRecipe({ recipe }: { recipe: { title: string; url: string } }) {
  if (can('share') === 'none') return null;
  return (
    <button
      onClick={async () => {
        const ok = await share({ title: recipe.title, text: 'Try this recipe', url: recipe.url });
        if (ok) toast('Shared');           // false = the user closed the sheet; say nothing
      }}
    >
      Share
    </button>
  );
}

Hanya teks (tanpa tautan) juga boleh:

import { share } from '@/lib/appmintNative';

await share({ text: 'My score today: 42' });

Bentuk ala Capacitor Share.share(opts) memanggil kode yang sama tetapi MENOLAK saat pengguna membatalkan (kontrak Capacitor) dan selesai dengan { activityType: '' } jika tidak; Share.canShare() selesai { value: can('share') !== 'none' }:

import { Share } from '@/lib/appmintNative';

if ((await Share.canShare()).value) {
  try {
    await Share.share({ title: 'Invite', url: 'https://example.com/join' });
  } catch {
    /* cancelled or unavailable — never report success here */
  }
}

Catatan:

  • Berikan hanya kolom yang Anda punya. Di aplikasi, teks dan tautan melewati plugin Share Capacitor.
  • files berfungsi di aplikasi Android (file masuk ke lembar bagikan sebagai lampiran asli; url ditambahkan ke teks) dan di web jika browser bisa membagikan file (navigator.canShare({ files })). Jaga file tetap kecil - file diteruskan ke aplikasi lewat memori.
  • Di web, panggil dari ketukan; browser menolak berbagi yang tidak dimulai oleh aksi pengguna.

appmintNative.Share appmintNative#

import { Share } from '@/lib/appmintNative'

Contoh

Didokumentasikan bersama appmintNative.share - contoh di sana menunjukkan yang ini juga.

Membuka sheet berbagi sistem di aplikasi, dan Web Share API di browser.

Mengembalikan: Promise<boolean> - true jika sheet terbuka; false jika pengguna menutupnya atau berbagi tidak tersedia. Ditolak jika isi berbagi tidak valid (misalnya url yang tidak didukung) atau ada berbagi lain yang masih terbuka. Dibutuhkan: tidak ada.

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

export function ShareRecipe({ recipe }: { recipe: { title: string; url: string } }) {
  if (can('share') === 'none') return null;
  return (
    <button
      onClick={async () => {
        const ok = await share({ title: recipe.title, text: 'Try this recipe', url: recipe.url });
        if (ok) toast('Shared');           // false = the user closed the sheet; say nothing
      }}
    >
      Share
    </button>
  );
}

Hanya teks (tanpa tautan) juga boleh:

import { share } from '@/lib/appmintNative';

await share({ text: 'My score today: 42' });

Bentuk ala Capacitor Share.share(opts) memanggil kode yang sama tetapi MENOLAK saat pengguna membatalkan (kontrak Capacitor) dan selesai dengan { activityType: '' } jika tidak; Share.canShare() selesai { value: can('share') !== 'none' }:

import { Share } from '@/lib/appmintNative';

if ((await Share.canShare()).value) {
  try {
    await Share.share({ title: 'Invite', url: 'https://example.com/join' });
  } catch {
    /* cancelled or unavailable — never report success here */
  }
}

Catatan:

  • Berikan hanya kolom yang Anda punya. Di aplikasi, teks dan tautan melewati plugin Share Capacitor.
  • files berfungsi di aplikasi Android (file masuk ke lembar bagikan sebagai lampiran asli; url ditambahkan ke teks) dan di web jika browser bisa membagikan file (navigator.canShare({ files })). Jaga file tetap kecil - file diteruskan ke aplikasi lewat memori.
  • Di web, panggil dari ketukan; browser menolak berbagi yang tidak dimulai oleh aksi pengguna.

appmintNative.sharedText appmintNative#

sharedText(): { text, subject, url, receivedAt } | null

Contoh

Memberikan teks atau tautan yang dibagikan aplikasi lain KE aplikasi ini ("Bagikan → aplikasi Anda"), misalnya tautan yang dibagikan dari browser atau YouTube.

Mengembalikan: secara sinkron, { text, subject, url, receivedAt } atau null jika tidak ada yang menunggu. url adalah tautan http(s) pertama yang ditemukan di text ('' jika tidak ada); receivedAt adalah milidetik sejak 1970. Membutuhkan: opsi Receive Shares From Other Apps pada build dengan "Teks dan tautan" - diaktifkan otomatis saat kode Anda memanggil sharedText( atau onShared(.

Periksa saat dimuat, simpan, lalu hapus:

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

export function useSaveSharedLink(save: (item: { title: string; url: string; text: string }) => Promise<void>) {
  useEffect(() => {
    if (can('shareTarget') === 'none') return;     // on the web
    const p = sharedText();
    if (!p) return;
    void save({ title: p.subject || p.url || 'Shared note', url: p.url, text: p.text })
      .then(() => { clearShared(); toast('Saved'); });
  }, [save]);
}

Dengarkan berbagi yang masuk saat aplikasi sudah terbuka dengan onShared, dan hapus setelah dipakai.

import { useEffect, useState } from 'react';
import { sharedText, onShared, clearShared } from '@/lib/appmintNative';

export function useIncomingShare() {
  const [text, setText] = useState(() => sharedText()?.text ?? '');
  useEffect(() => onShared((p) => setText(p.text)), []);
  return { text, done: () => { clearShared(); setText(''); } };
}

Isi awal formulir dari tautan yang dibagikan dan jangan tampilkan apa pun di web, di mana sharedText() adalah null.

import { useEffect, useState } from 'react';
import { can, sharedText } from '@/lib/appmintNative';

export function NewBookmark() {
  const [url, setUrl] = useState('');
  useEffect(() => {
    if (can('shareTarget') === 'none') return;   // web: nobody can share into the page
    const p = sharedText();
    if (p?.url) setUrl(p.url);
  }, []);
  return <input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="Paste a link" />;
}

Catatan:

  • Berbagi tetap DISIMPAN sampai Anda memanggil clearShared(), sehingga aplikasi yang dimuat setelah berbagi tetap melihatnya. Hapus setelah menyimpan, atau pemuatan berikutnya akan menyimpannya lagi.
  • Untuk berbagi yang masuk saat aplikasi terbuka, gunakan onShared().
  • Foto atau video yang dibagikan DENGAN keterangan: keterangan tiba di sini, filenya lewat API file yang dibuka.
  • Hanya aplikasi Android. can('shareTarget') bernilai 'none' di web, dan sharedText() mengembalikan null di sana.

appmintNative.sheet appmintNative#

sheet(opts: { title?, content, dismissible? }): { close(), element, closed }

Contoh

Membuka bottom sheet modal dengan perilaku sheet yang sebenarnya - untuk panel filter, penulis pesan, atau pratinjau detail.

Mengembalikan: sebuah handle: close() menutupnya, element adalah elemen isi (isi atau perbarui), closed adalah Promise<void> yang selesai saat lembar tertutup, bagaimanapun caranya. Membutuhkan: tidak ada.

Opsi: title (judul opsional), content - elemen, teks biasa (bukan HTML), atau fungsi (host) => { … } yang mengisi isi; dismissible (bawaan true: ketuk latar, tarik ke bawah, Escape, dan gestur kembali menutupnya).

Konten DOM biasa:

import { sheet } from '@/lib/appmintNative';

const h = sheet({
  title: 'About this list',
  content: (host) => {
    const p = document.createElement('p');
    p.textContent = 'Items sync to all your devices.';
    host.appendChild(p);
  },
});
await h.closed;

Konten React, dirender ke dalam sheet dengan portal:

import { useState } from 'react';
import { createPortal } from 'react-dom';
import { sheet } from '@/lib/appmintNative';

export function FiltersButton() {
  const [host, setHost] = useState<HTMLElement | null>(null);
  const [close, setClose] = useState<() => void>(() => () => {});

  const open = () => {
    const h = sheet({ title: 'Filters', content: () => {} });
    setHost(h.element);
    setClose(() => h.close);
    void h.closed.then(() => setHost(null));      // unmount the portal after any close
  };

  return (
    <>
      <button onClick={open}>Filters</button>
      {host && createPortal(
        <form onSubmit={(e) => { e.preventDefault(); close(); }}>
          <label><input type="checkbox" name="open" /> Open now</label>
          <button type="submit">Apply</button>
        </form>,
        host,
      )}
    </>
  );
}

Lembar yang tetap terbuka sampai pengguna memilih, memakai dismissible: false dan promise closed.

import { sheet } from '@/lib/appmintNative';

export async function confirmDelete(name: string): Promise<boolean> {
  let answer = false;
  const h = sheet({
    title: 'Delete this item?',
    dismissible: false,                       // only the buttons below close it
    content: (host) => {
      const p = document.createElement('p');
      p.textContent = `"${name}" will be removed.`;
      const yes = document.createElement('button');
      yes.textContent = 'Delete';
      yes.onclick = () => { answer = true; h.close(); };
      const no = document.createElement('button');
      no.textContent = 'Keep it';
      no.onclick = () => h.close();
      host.append(p, yes, no);
    },
  });
  await h.closed;                             // resolves however the sheet closed
  return answer;
}

Catatan:

  • Digambar dengan token aplikasi (--surface, --ink, --muted, --border), di atas area aman, dengan haptic ringan saat dibuka.
  • Tarik ke bawah menutupnya jika ditarik lebih dari 120 px atau disentil; isi digulir lebih dulu saat belum berada di atas.
  • Dengan dismissible: false hanya close() milik Anda yang menutupnya - berikan tombol kepada pengguna.
  • Untuk daftar tindakan biasa gunakan actionSheet().

appmintNative.shortcuts appmintNative#

shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()

Contoh

Menambahkan pintasan ke menu tekan lama ikon aplikasi (pintasan dinamis Android). Satu ketukan membuka aplikasi pada rute tersebut.

Mengembalikan: set() mengembalikan Promise<boolean> - true saat pintasan diserahkan ke sistem, false di tempat yang tidak punya (web). clear() mengembalikan Promise<void>. Membutuhkan: tidak ada.

Atur sekali saat mulai, lalu arahkan berdasarkan tautan peluncuran:

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { can, shortcuts, launchUrl } from '@/lib/appmintNative';

export function useAppShortcuts() {
  const navigate = useNavigate();
  useEffect(() => {
    if (can('shortcuts') !== 'none') {
      void shortcuts.set([
        { id: 'new', title: 'New note', route: '/new' },
        { id: 'search', title: 'Search', route: '/search' },
      ]);
    }
    void launchUrl().then((u) => { if (u) navigate(new URL(u).pathname); });
  }, [navigate]);
}

Hapus semuanya (misalnya setelah keluar):

import { shortcuts } from '@/lib/appmintNative';

await shortcuts.clear();

Buka layar yang sesuai saat pintasan diketuk ketika aplikasi sudah berjalan, dengan onDeepLink.

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

export function useShortcutRouting() {
  const navigate = useNavigate();
  useEffect(() => onDeepLink((url) => navigate(new URL(url).pathname)), [navigate]);
}

Tampilkan pintasan hanya untuk pengguna yang masuk, dan tangani web, di mana set() menjawab false.

import { useEffect } from 'react';
import { shortcuts } from '@/lib/appmintNative';

export function useAccountShortcuts(signedIn: boolean) {
  useEffect(() => {
    if (!signedIn) { void shortcuts.clear(); return; }
    void shortcuts.set([
      { id: 'inbox', title: 'Inbox', route: '/inbox' },
      { id: 'compose', title: 'Write a message', route: 'compose' },   // gets a leading "/"
    ]).then((handed) => { if (!handed) console.info('No launcher shortcuts on this platform'); });
  }, [signedIn]);
}

Catatan:

  • Maksimal 4 item; item tambahan dibuang. route tanpa awalan / akan ditambahkan satu. id bersifat opsional.
  • Android 7.1+; di Android yang lebih lama panggilan ini tidak berpengaruh. Judul dipotong menjadi 25 karakter (label pendek) dan 50 (label panjang).
  • Ketukan membuka aplikasi dengan tautan peluncuran appmint-shortcut://<package><route>, jadi new URL(u).pathname adalah rute Anda. Baca dengan launchUrl() saat dipasang dan onDeepLink() saat aplikasi berjalan.
  • can('shortcuts') adalah 'none' di web: browser tidak punya ikon aplikasi.

appmintNative.snoozeAlarm appmintNative#

snoozeAlarm(id?, minutes?) — @/lib/appmintNative

Contoh

Menghentikan alarm yang berbunyi dan membunyikannya lagi setelah minutes (0 atau dihilangkan = snoozeMinutes milik alarm itu sendiri, bawaan 10). Dering besok dari alarm berulang tidak bergeser.

Mengembalikan: boolean - true jika alarm sedang berdering dan kini ditunda. Dibutuhkan: 🔔 Alarm clock di langkah Akses (Langkah 4) saat Anda membuat aplikasi.

import { isAlarmRinging, snoozeAlarm } from '@/lib/appmintNative';

// A shake-to-snooze handler:
window.addEventListener('devicemotion', (e) => {
  const a = e.accelerationIncludingGravity;
  if (a && Math.abs(a.x ?? 0) > 25 && isAlarmRinging()) snoozeAlarm('', 5);
});

Tunda dari tombol di layar alarm Anda sendiri, memakai onAlarm untuk mengetahui kapan alarm berbunyi.

import { useEffect, useState } from 'react';
import { onAlarm, snoozeAlarm, stopAlarm } from '@/lib/appmintNative';

export function RingingScreen() {
  const [ringing, setRinging] = useState(false);
  useEffect(() => onAlarm((e) => setRinging(e.action === 'open' && e.ringing === true)), []);
  if (!ringing) return null;
  return (
    <div>
      <button onClick={() => { if (snoozeAlarm('', 10)) setRinging(false); }}>Snooze 10 min</button>
      <button onClick={() => { stopAlarm(); setRinging(false); }}>Stop</button>
    </div>
  );
}

snoozeAlarm menjawab false jika tidak ada yang berdering, jadi periksa sebelum menjanjikan dering kedua kepada pengguna.

import { snoozeAlarm } from '@/lib/appmintNative';

export function snoozeOrExplain(minutes = 0) {
  return snoozeAlarm('', minutes)
    ? `Ringing again in ${minutes || 'a few'} minutes`
    : 'No alarm is ringing right now.';
}

appmintNative.speak appmintNative#

speak(text, opts?) — @/lib/appmintNative

Contoh

Membacakan teks dengan mesin text-to-speech ponsel.

Mengembalikan: void - mulai berbicara (atau mengantre setelah ucapan saat ini) dan tidak memberi sinyal "selesai". opts: { lang?: string; rate?: number; pitch?: number } (bawaan: bahasa mesin, 1, 1). Membutuhkan: tidak ada.

import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadAloud({ text }: { text: string }) {
  return (
    <div>
      <button onClick={() => speak(text, { lang: 'en-IN', rate: 0.9 })}>Read aloud</button>
      <button onClick={() => stopSpeaking()}>Stop</button>
    </div>
  );
}

Bahasa dan kecepatan yang berbeda:

import { speak } from '@/lib/appmintNative';

speak('வணக்கம்', { lang: 'ta-IN' });          // Tamil
speak('Slow and clear', { rate: 0.7 });       // rate 0.1..4, 1 = normal
speak('High voice', { pitch: 1.5 });          // pitch 0.1..4, 1 = normal

Ucapkan konfirmasi singkat setelah sebuah tindakan, dan beri tahu pengguna dalam bentuk teks juga.

import { speak, toast } from '@/lib/appmintNative';

export function announceSaved(title: string) {
  toast('Saved');
  speak(`${title} saved`, { rate: 1.1 });
}

Bacakan daftar satu item demi satu item; speak() mengantre, jadi panggil dalam loop dan hentikan semuanya dengan stopSpeaking() saat layar ditutup.

import { useEffect } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadHeadlines({ items }: { items: string[] }) {
  useEffect(() => () => stopSpeaking(), []);   // leaving the screen stops the voice
  return <button onClick={() => items.forEach((t) => speak(t))}>Read all</button>;
}

Catatan: Di aplikasi Android: mesin native aplikasi. Di web: speechSynthesis. Beberapa panggilan speak() diputar satu per satu. Jika perlu tahu kapan ucapan selesai (untuk menyorot kata atau lanjut), gunakan window.speechSynthesis dengan onend milik utterance secara langsung; ini berfungsi di aplikasi Android juga.

appmintNative.speech appmintNative#

speech — @/lib/appmintNative

Contoh

Dikte: mengubah ucapan pengguna menjadi teks. Pengenal suara Android di aplikasi Android, Web Speech API di Chrome.

Mengembalikan: speech.isAvailable() → Promise<boolean>. speech.start(onResult, opts?) → Promise<boolean>: true saat mendengarkan dimulai, false saat izin mikrofon ditolak atau dikte tidak tersedia. onResult(text, isFinal) dipanggil dengan teksnya. speech.stop() → Promise<void>. opts: { lang?: string; partial?: boolean; prompt?: string }. Membutuhkan: Mic di Langkah 4 (Akses); build AI mengaktifkannya untuk Anda saat kode memakai pengenalan suara.

import { useEffect, useState } from 'react';
import { speech } from '@/lib/appmintNative';

export function VoiceNote() {
  const [ok, setOk] = useState(false);
  const [text, setText] = useState('');
  const [msg, setMsg] = useState('');

  useEffect(() => { void speech.isAvailable().then(setOk); }, []);
  if (!ok) return null;                      // no dictation here: hide the mic button

  const listen = async () => {
    setMsg('Listening...');
    const started = await speech.start(
      (t, isFinal) => { setText(t); if (isFinal) setMsg(''); },
      { lang: 'en-US' },                     // partial results by default
    );
    if (!started) setMsg('Microphone permission is needed for voice input.');
  };

  return (
    <div>
      <button aria-label="Speak" onClick={listen}>🎤</button>
      <button onClick={() => { void speech.stop(); setMsg(''); }}>Stop</button>
      <p>{text}</p>
      <p role="status">{msg}</p>
    </div>
  );
}

Dikte ke kolom teks dan beri tahu jika mikrofon ditolak, karena start() menjawab false.

import { useState } from 'react';
import { speech } from '@/lib/appmintNative';

export function DictateField() {
  const [value, setValue] = useState('');
  const [error, setError] = useState('');

  const dictate = async () => {
    setError('');
    const started = await speech.start((text, isFinal) => { if (isFinal) setValue((v) => (v ? `${v} ${text}` : text)); }, { partial: false });
    if (!started) setError('Voice input is not available. Check the microphone permission.');
  };

  return (
    <div>
      <textarea value={value} onChange={(e) => setValue(e.target.value)} />
      <button onClick={dictate}>Dictate</button>
      {error && <p role="alert">{error}</p>}
    </div>
  );
}

Berhenti mendengarkan saat layar ditutup, dan baca hasilnya kembali dengan speak.

import { useEffect, useState } from 'react';
import { speech, speak } from '@/lib/appmintNative';

export function VoiceCommand() {
  const [heard, setHeard] = useState('');
  useEffect(() => () => { void speech.stop(); }, []);

  return (
    <button
      onClick={() => void speech.start((t, isFinal) => {
        setHeard(t);
        if (isFinal) speak(`You said ${t}`);
      }, { lang: 'en-US' })}
    >
      {heard || 'Say something'}
    </button>
  );
}

Catatan:

  • Dengan partial aktif (default) onResult(text, false) datang saat kata-kata masuk, lalu sekali onResult(text, true) dengan teks akhir. Dengan partial: false Anda hanya mendapat yang akhir, saat pengguna berhenti bicara.
  • Prompt izin ditampilkan pada start().
  • Jika pengguna tidak mengatakan apa-apa, onResult tidak dipanggil; mendengarkan hanya berakhir. Sediakan tombol Berhenti atau batas waktu di UI Anda.
  • Mendengarkan berhenti sendiri setelah jeda. lang secara default memakai bahasa ponsel.

appmintNative.splash appmintNative#

splash.hold() / splash.ready()

Contoh

Menahan splash peluncuran di layar sampai layar nyata pertama siap, alih-alih menampilkan "splash, lalu spinner".

Mengembalikan: tidak ada (keduanya sinkron). Membutuhkan: tidak ada.

Secara default splash tersembunyi sendiri pada gambar pertama aplikasi - Anda tidak perlu melakukan apa-apa. Jika layar pertama membutuhkan data, tahan splash di bagian atas modul dan lepaskan saat layar itu tampil:

// main.tsx
import { splash } from '@/lib/appmintNative';

splash.hold();   // at import time, before the first render
// Home.tsx
import { useEffect, useState } from 'react';
import { splash } from '@/lib/appmintNative';

export function Home({ load }: { load: () => Promise<string[]> }) {
  const [items, setItems] = useState<string[] | null>(null);

  useEffect(() => {
    load()
      .then(setItems)
      .catch(() => setItems([]))
      .finally(() => splash.ready());     // ALWAYS release, also when loading failed
  }, [load]);

  if (!items) return null;
  return <ul>{items.map((t) => <li key={t}>{t}</li>)}</ul>;
}

Tahan splash saat sesi yang sudah masuk dipulihkan, dan lepaskan di finally agar kegagalan tidak membuatnya tertahan.

// main.tsx
import { splash } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';

splash.hold();
supabase.auth.getSession().finally(() => splash.ready());

Lepaskan sekali, dari hook, setelah data layar pertama tampil.

import { useEffect } from 'react';
import { splash } from '@/lib/appmintNative';

export function useReleaseSplash(loaded: boolean) {
  useEffect(() => {
    if (loaded) splash.ready();   // calling it again later does nothing harmful
  }, [loaded]);
}

Catatan:

  • Di aplikasi Android, hold() memberi tahu shell untuk mempertahankan splash-nya; ready() menyembunyikannya (waktu splash minimum dari pembuat tetap berlaku).
  • Di web keduanya tidak melakukan apa-apa - tidak ada splash.
  • Shell membatasi penahanan sekitar 6 detik, jadi halaman yang tidak pernah memanggil ready() tetap terbuka.

appmintNative.StatusBar appmintNative#

StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })

Contoh

Bilah status dalam bentuk plugin StatusBar milik Capacitor, dibangun di atas setStatusBar().

Mengembalikan: Promise<void> untuk keduanya. Membutuhkan: tidak ada.

import { StatusBar } from '@/lib/appmintNative';

// Colour only — the icon style is left alone (the shell picks readable icons on Android).
await StatusBar.setBackgroundColor({ color: '#0B0F1A' });

Warna dan ikon sekaligus - utamakan setStatusBar(), yang mengatur keduanya dalam satu panggilan:

import { setStatusBar } from '@/lib/appmintNative';

setStatusBar('#0B0F1A', 'light');   // dark bar, light icons
setStatusBar('#FFFFFF', 'dark');    // light bar, dark icons

Ganti bilah mengikuti tema terang dan gelap aplikasi sendiri, dengan menggabungkan panggilan bergaya Capacitor.

import { useEffect } from 'react';
import { StatusBar } from '@/lib/appmintNative';

export function useStatusBarForTheme(dark: boolean) {
  useEffect(() => {
    void StatusBar.setBackgroundColor({ color: dark ? '#0B0F1A' : '#FFFFFF' });
    void StatusBar.setStyle({ style: dark ? 'DARK' : 'LIGHT' });   // 'DARK' = dark bar, light icons
  }, [dark]);
}

Catatan: style mengikuti Capacitor: 'DARK' berarti latar gelap (ikon terang), selain itu berarti ikon gelap. Di aplikasi Android, bridge shell mewarnai bar (panggilan warna dari plugin tidak berpengaruh di Android 15+), dan setStyle() membalik ikon sesuai warna bar saat ini. Di web warnanya menjadi <meta name="theme-color">.

appmintNative.stopAlarm appmintNative#

stopAlarm(id?) — @/lib/appmintNative

Contoh

Menghentikan alarm yang sedang berbunyi, seperti tombol Tutup-nya. Alarm berulang tetap diatur untuk waktu berikutnya.

Mengembalikan: boolean - true jika alarm sedang berdering dan kini dihentikan. id dihilangkan = yang sedang berdering. Dibutuhkan: 🔔 Alarm clock di langkah Akses (Langkah 4) saat Anda membuat aplikasi.

import { stopAlarm, snoozeAlarm } from '@/lib/appmintNative';

export function RingScreen({ id, onClose }: { id: string; onClose: () => void }) {
  return (
    <div className="ring">
      <button onClick={() => { snoozeAlarm(id, 10); onClose(); }}>Snooze 10 min</button>
      <button onClick={() => { stopAlarm(id); onClose(); }}>Stop</button>
    </div>
  );
}

Hentikan alarm dari layar yang dibuka aplikasi saat alarm berbunyi, dan tanggapi setiap cara alarm berakhir:

import { useEffect, useState } from 'react';
import { onAlarm, isAlarmRinging, stopAlarm } from '@/lib/appmintNative';

export function useRingingAlarm() {
  const [ringingId, setRingingId] = useState<string | null>(null);
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open' && e.ringing) setRingingId(e.id);
    else setRingingId(null);   // dismiss, snooze, missed or error: it is no longer ringing
  }), []);
  return {
    ringingId,
    stop: () => { if (isAlarmRinging()) stopAlarm(ringingId ?? ''); },
  };
}

Periksa jawabannya: false berarti tidak ada yang sedang berdering (atau ini adalah web), jadi jangan klaim bahwa alarm sudah berhenti:

import { stopAlarm, alarmSupport } from '@/lib/appmintNative';

export function stopWithFeedback(id: string, say: (m: string) => void) {
  if (alarmSupport() === 'unavailable') return say('Alarms only work in the Android app');
  say(stopAlarm(id) ? 'Alarm stopped' : 'That alarm is not ringing');
}

appmintNative.stopSpeaking appmintNative#

stopSpeaking() — @/lib/appmintNative

Contoh

Menghentikan text-to-speech sekarang dan membuang semua yang masih menunggu untuk diucapkan.

Mengembalikan: void. Membutuhkan: tidak ada.

import { useEffect } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function StoryPage({ story }: { story: string }) {
  // Stop reading when the user leaves the page.
  useEffect(() => () => stopSpeaking(), []);

  return (
    <article>
      <p>{story}</p>
      <button onClick={() => { stopSpeaking(); speak(story); }}>Read from the start</button>
      <button onClick={stopSpeaking}>Stop</button>
    </article>
  );
}

Bacakan teks panjang dengan bahasa dan kecepatan pilihan pengguna, dan hentikan dengan satu tombol:

import { useState } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadAloud({ text, lang }: { text: string; lang: string }) {
  const [rate, setRate] = useState(1);
  return (
    <div>
      <input type="range" min={0.5} max={2} step={0.1} value={rate} onChange={(e) => setRate(Number(e.target.value))} />
      <button onClick={() => { stopSpeaking(); speak(text, { lang, rate }); }}>Read aloud</button>
      <button onClick={() => stopSpeaking()}>Stop</button>
    </div>
  );
}

Potong suara saat pengguna mulai mengetik balasan:

import { stopSpeaking } from '@/lib/appmintNative';

export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
  return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}

Catatan: Aman dipanggil saat tidak ada yang berbicara. Di aplikasi Android: perintah berhenti dari mesin native. Di web: speechSynthesis.cancel().

appmintNative.swipeable appmintNative#

swipeable(el: HTMLElement, opts: SwipeOptions): () => void

Contoh

Menambahkan gestur geser ke sebuah elemen - baris geser-untuk-hapus, paging, menutup kartu.

Mengembalikan: fungsi berhenti berlangganan yang juga memulihkan touch-action asli elemen. Dibutuhkan: tidak ada.

Opsi: onLeft, onRight, onUp, onDown (minimal satu), threshold (px gerakan, bawaan 56), edgeGuard (px dari tepi layar kiri/kanan di mana usapan horizontal diabaikan, bawaan 24), haptics (haptic seleksi saat usapan, bawaan true).

Geser ke kiri untuk menghapus, dengan tombol terlihat untuk aksi yang sama:

import { useEffect, useRef } from 'react';
import { swipeable, toast } from '@/lib/appmintNative';

export function MessageRow({ id, text, onDelete, onUndo }: {
  id: string; text: string; onDelete(id: string): void; onUndo(id: string): void;
}) {
  const ref = useRef<HTMLLIElement>(null);
  useEffect(() => {
    if (!ref.current) return;
    return swipeable(ref.current, {
      onLeft: () => { onDelete(id); toast('Deleted'); },
    });
  }, [id, onDelete]);

  return (
    <li ref={ref} className="flex items-center justify-between p-4">
      <span>{text}</span>
      <button onClick={() => onDelete(id)}>Delete</button>
      {/* swipe is never the only way to an action */}
    </li>
  );
}

Pindah antar tab dengan kiri / kanan:

import { swipeable } from '@/lib/appmintNative';

declare const panel: HTMLElement;
declare let tab: number;
const off = swipeable(panel, { onLeft: () => { tab += 1; }, onRight: () => { tab -= 1; }, threshold: 72 });
// later: off();

Tutup kartu dengan menggesernya ke atas, dengan handler dilepas saat komponen dilepas:

import { useEffect, useRef } from 'react';
import { swipeable } from '@/lib/appmintNative';

export function Banner({ onClose, children }: { onClose: () => void; children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => (ref.current ? swipeable(ref.current, { onUp: onClose, threshold: 40 }) : undefined), [onClose]);
  return <div ref={ref} className="banner">{children}</div>;
}

Catatan:

  • Dibangun di atas Pointer Events, sehingga kode yang sama berfungsi dengan sentuhan, pena, dan seretan mouse.
  • Ini mengatur touch-action untuk Anda: pan-y untuk handler horizontal (gulir vertikal tetap berfungsi), pan-x untuk yang vertikal, none untuk keduanya.
  • Gesekan harus jelas satu arah (1,5 kali lebih banyak pada satu sumbu); gulir diagonal tidak memicu. Jari kedua membatalkan gestur.
  • Penjaga tepi ada karena gestur kembali sistem Android menguasai tepi layar.

appmintNative.systemPalette appmintNative#

systemPalette(): Promise<SystemPalette>

Contoh

Membaca warna Material You ponsel (dibuat dari wallpaper di Android 12+), sehingga warna aksen aplikasi bisa mengikuti wallpaper pengguna seperti aplikasi sistem.

Mengembalikan: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - setiap swatch memetakan satu tone ('0', '10', '50', '100' … '900', '1000') ke warna hex. { available: false } di bawah Android 12 dan di web. Membutuhkan: tidak ada.

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

export async function applyWallpaperAccent() {
  const p = await systemPalette();
  if (!p.available || !p.accent1) return;          // keep the app's own palette
  const root = document.documentElement.style;
  root.setProperty('--accent', p.accent1['600']);
  root.setProperty('--accent-soft', p.accent1['100']);
}

Gunakan warna wallpaper hanya jika ponsel menawarkannya, dan baca ulang saat aplikasi kembali ke layar:

import { useEffect, useState } from 'react';
import { systemPalette } from '@/lib/appmintNative';

export function useWallpaperAccent(enabled: boolean) {
  const [accent, setAccent] = useState<string | null>(null);
  useEffect(() => {
    if (!enabled) { setAccent(null); return; }
    const read = () => systemPalette().then((p) => setAccent(p.available ? p.accent1?.['600'] ?? null : null));
    void read();
    document.addEventListener('visibilitychange', read);
    return () => document.removeEventListener('visibilitychange', read);
  }, [enabled]);
  return accent;   // null = no wallpaper palette on this device (Android 11 and older, or the web)
}

Tampilkan sakelar "Sesuaikan dengan wallpaper saya" hanya jika palet tersedia, dengan { available: false } sebagai jawaban untuk "tidak di sini":

import { useEffect, useState } from 'react';
import { can, systemPalette } from '@/lib/appmintNative';

export function WallpaperToggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) {
  const [ok, setOk] = useState(false);
  useEffect(() => { if (can('systemPalette') !== 'none') systemPalette().then((p) => setOk(p.available)); }, []);
  if (!ok) return null;
  return <label><input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} /> Match my wallpaper</label>;
}

Catatan: Periksa kontras sebelum memakai warna wallpaper untuk teks atau tombol - --accent harus tetap mencapai 3:1 pada latar Anda. Tawarkan sebagai opsi ("Samakan dengan wallpaper saya"), bukan tema yang dipaksakan. Panggil lagi saat aplikasi dilanjutkan; pengguna mungkin telah mengganti wallpaper.

appmintNative.TextToSpeech appmintNative#

TextToSpeech — @/lib/appmintNative

Contoh

Objek TextToSpeech ala Capacitor (speak / stop), berjalan pada kode yang sama dengan speak() dan stopSpeaking().

Mengembalikan: speak({ text, lang?, rate?, pitch? }) → Promise<void> yang selesai setelah teks diucapkan (atau stop() memotongnya) dan menolak jika tidak bisa diucapkan. stop() → Promise<void>. Membutuhkan: tidak ada.

import { TextToSpeech } from '@/lib/appmintNative';

async function announce(orderNo: number) {
  await TextToSpeech.speak({ text: `Order ${orderNo} is ready`, lang: 'en-US', rate: 1, pitch: 1 });
  // Here the text is queued; it may still be speaking.
}

async function silence() {
  await TextToSpeech.stop();
}

Bacakan daftar baris satu per satu; setiap speak selesai saat diantrekan, sehingga mesin memutarnya berurutan:

import { TextToSpeech } from '@/lib/appmintNative';

export async function readSteps(steps: string[]) {
  await TextToSpeech.stop();   // drop anything still waiting from before
  for (const [i, s] of steps.entries()) {
    await TextToSpeech.speak({ text: `Step ${i + 1}. ${s}`, lang: 'en-US' });
  }
}

Tombol baca-keras yang berhenti berbicara saat layar ditinggalkan:

import { useEffect } from 'react';
import { TextToSpeech } from '@/lib/appmintNative';

export function ArticleReader({ text }: { text: string }) {
  useEffect(() => () => { void TextToSpeech.stop(); }, []);
  return <button onClick={() => void TextToSpeech.speak({ text, rate: 0.9 })}>Listen</button>;
}

Catatan: await TextToSpeech.speak(…) adalah sinyal selesai: langkah berikutnya setelah berbicara bisa langsung mengikutinya. Di aplikasi: mesin text-to-speech Android native milik shell; di web: speechSynthesis.

appmintNative.textZoom appmintNative#

textZoom.get() / textZoom.getPreferred() / textZoom.set(value)

Contoh

Kontrol ukuran teks dalam aplikasi. Di aplikasi, ini mengubah zoom teks halaman (1 = 100%); di web, ini mengubah ukuran font root, yang menskalakan setiap tata letak berbasis rem.

Mengembalikan: get() → Promise<number>. getPreferred() → Promise<number> - ukuran yang dipilih pengguna di pengaturan ponsel (1 di web). set(value) → Promise<void>; nilainya dibatasi ke 0.5-3, dan ditolak jika nilainya bukan angka. Membutuhkan: tidak ada.

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

async function bigger() { await textZoom.set((await textZoom.get()) + 0.1); }
async function smaller() { await textZoom.set((await textZoom.get()) - 0.1); }
async function reset() { await textZoom.set(await textZoom.getPreferred()); }

Ingat pilihan di setiap peluncuran:

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

const saved = Number(localStorage.getItem('textZoom'));
if (saved) await textZoom.set(saved);

export async function setSize(v: number) {
  await textZoom.set(v);
  localStorage.setItem('textZoom', String(v));
}

Stepper ukuran teks yang membaca nilai saat ini, menjaganya dalam rentang yang diizinkan, dan menampilkannya dalam persen:

import { useEffect, useState } from 'react';
import { textZoom } from '@/lib/appmintNative';

export function TextSizeStepper() {
  const [z, setZ] = useState(1);
  useEffect(() => { textZoom.get().then(setZ); }, []);
  const change = async (d: number) => {
    const next = Math.min(3, Math.max(0.5, Math.round((z + d) * 10) / 10));
    await textZoom.set(next);
    setZ(next);
  };
  return (
    <div>
      <button onClick={() => change(-0.1)}>A-</button>
      <span>{Math.round(z * 100)}%</span>
      <button onClick={() => change(0.1)}>A+</button>
    </div>
  );
}

Catatan: Aplikasi sudah mengikuti pengaturan ukuran font ponsel; ini adalah kontrol tambahan di dalam aplikasi. Uji tata letak pada ukuran besar - tinggi tetap pada kontrol teks akan terpotong.

appmintNative.toast appmintNative#

toast(message: string, opts?: { duration?: 'short' | 'long' }): void

Contoh

Menampilkan pesan singkat yang tidak perlu dijawab - "Tersalin", "Tersimpan", "Ditambahkan ke daftar".

Mengembalikan: tidak ada (kirim dan lupakan). Membutuhkan: tidak ada.

import { toast, haptic } from '@/lib/appmintNative';

export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await onSave();
        haptic('success');
        toast('Saved');                              // 'short' ≈ 2 s
      }}
    >
      Save
    </button>
  );
}

Pesan yang lebih panjang tampil sekitar 3,5 detik:

import { toast } from '@/lib/appmintNative';

toast('Your export will be ready in a minute', { duration: 'long' });

Bentuk ala Capacitor Toast.show({ text, duration? }) melakukan hal yang sama dan menghasilkan Promise<void>:

import { Toast } from '@/lib/appmintNative';

await Toast.show({ text: 'Added to list' });

Untuk pesan yang WAJIB dilihat pengguna, gunakan dialog:

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

Catatan:

  • Di aplikasi Android, plugin Capacitor Toast menampilkannya di bagian bawah layar.
  • Di tempat lain berupa snackbar kecil yang digambar di atas halaman dengan warna aplikasi sendiri (--ink pada --bg), di atas inset area aman, dengan role="status" agar pembaca layar mengumumkannya.
  • Hanya satu yang tampil pada satu waktu; toast baru menggantikan yang lama.
  • Toast memang mudah terlewat. Jangan pernah memakainya untuk kesalahan yang harus ditindaklanjuti pengguna.

appmintNative.Toast appmintNative#

import { Toast } from '@/lib/appmintNative'

Contoh

Didokumentasikan bersama appmintNative.toast - contoh di sana menunjukkan yang ini juga.

Menampilkan pesan singkat yang tidak perlu dijawab - "Tersalin", "Tersimpan", "Ditambahkan ke daftar".

Mengembalikan: tidak ada (kirim dan lupakan). Membutuhkan: tidak ada.

import { toast, haptic } from '@/lib/appmintNative';

export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await onSave();
        haptic('success');
        toast('Saved');                              // 'short' ≈ 2 s
      }}
    >
      Save
    </button>
  );
}

Pesan yang lebih panjang tampil sekitar 3,5 detik:

import { toast } from '@/lib/appmintNative';

toast('Your export will be ready in a minute', { duration: 'long' });

Bentuk ala Capacitor Toast.show({ text, duration? }) melakukan hal yang sama dan menghasilkan Promise<void>:

import { Toast } from '@/lib/appmintNative';

await Toast.show({ text: 'Added to list' });

Untuk pesan yang WAJIB dilihat pengguna, gunakan dialog:

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

Catatan:

  • Di aplikasi Android, plugin Capacitor Toast menampilkannya di bagian bawah layar.
  • Di tempat lain berupa snackbar kecil yang digambar di atas halaman dengan warna aplikasi sendiri (--ink pada --bg), di atas inset area aman, dengan role="status" agar pembaca layar mengumumkannya.
  • Hanya satu yang tampil pada satu waktu; toast baru menggantikan yang lama.
  • Toast memang mudah terlewat. Jangan pernah memakainya untuk kesalahan yang harus ditindaklanjuti pengguna.

appmintNative.torch appmintNative#

import { torch } from '@/lib/appmintNative'

Contoh

Senter ponsel: torch.on(), off(), toggle(), isOn(), isAvailable(). can('torch') bernilai 'none' di web.

Mengembalikan: on()/off() → Promise (ditolak dengan code 'unavailable' atau 'CAMERA_IN_USE'); toggle() → status baru; isOn() / isAvailable() → boolean. Membutuhkan: tidak ada, tanpa izin.

import { useEffect, useState } from 'react';
import { can, torch } from '@/lib/appmintNative';

export function LightButton() {
  const [on, setOn] = useState(false);
  const [ok, setOk] = useState(false);
  useEffect(() => { if (can('torch') !== 'none') torch.isAvailable().then(setOk); }, []);
  if (!ok) return null;
  return (
    <button aria-pressed={on} aria-label="Flashlight" onClick={async () => setOn(await torch.toggle())}>
      {on ? 'Light off' : 'Light on'}
    </button>
  );
}

Matikan lampu saat pengguna meninggalkan layar dan tampilkan kesalahan yang dilaporkan ponsel saat lampu tidak bisa dinyalakan atau dimatikan:

import { useEffect, useState } from 'react';
import { torch } from '@/lib/appmintNative';

export function LightSwitch() {
  const [on, setOn] = useState(false);
  const [err, setErr] = useState('');
  useEffect(() => () => { void torch.off().catch(() => {}); }, []);
  const flip = async () => {
    try { setOn(await torch.toggle()); setErr(''); }
    catch (e) {
      const code = (e as { code?: string }).code;
      setErr(code === 'CAMERA_IN_USE' ? 'Close the camera first' : 'No flashlight on this device');
    }
  };
  return <div><button onClick={flip}>{on ? 'Off' : 'On'}</button>{err && <p role="alert">{err}</p>}</div>;
}

Kedipkan lampu sebagai sinyal, lalu biarkan mati:

import { can, torch } from '@/lib/appmintNative';

export async function blink(times = 3) {
  if (can('torch') === 'none') return;
  for (let i = 0; i < times; i++) {
    await torch.on();
    await new Promise((r) => setTimeout(r, 200));
    await torch.off();
    await new Promise((r) => setTimeout(r, 200));
  }
}

Catatan: dimatikan saat aplikasi ditutup. Selama stream kamera aplikasi sendiri terbuka, nyalakan dengan constraint torch pada track.

appmintNative.trackEvent appmintNative#

import { trackEvent } from '@/lib/appmintNative'

Contoh

Melaporkan momen di aplikasi Anda agar pesan dalam aplikasi kreator dengan pemicu "When an event happens" (nama yang sama) tampil. Aplikasi mengunduh kampanye kreator dari pusat pengiriman AppMint Push dan menampilkannya sendiri.

Mengembalikan: boolean secara sinkron: false untuk nama tidak valid (huruf, angka, dan _ . : -, hingga 64), di web, dan di aplikasi yang dibuat tanpa AppMint Push. Membutuhkan: Android: AppMint Push diaktifkan di langkah Integrasi (Langkah 3) saat Anda membuat build.

import { trackEvent } from '@/lib/appmintNative';

export async function completeCheckout(cart: { total: number }) {
  await saveOrder(cart);
  trackEvent('checkout_done', { total: cart.total });   // props are optional and not stored
}

Laporkan perubahan tampilan yang tidak terlihat di URL, agar pesan "On a page" melihatnya:

import { trackEvent } from '@/lib/appmintNative';

export function selectTab(tab: 'home' | 'cart' | 'profile') {
  setActiveTab(tab);
  trackEvent('appmint:route', { path: '/' + tab });
}

Hook yang melacak saat pertama kali sebuah layar dibuka:

import { useEffect } from 'react';
import { can, trackEvent } from '@/lib/appmintNative';

export function useTrackOnce(name: string) {
  useEffect(() => {
    if (can('inAppMessages') === 'none') return;
    const key = 'tracked:' + name;
    if (localStorage.getItem(key)) return;
    if (trackEvent(name)) localStorage.setItem(key, '1');
  }, [name]);
}

Catatan: perubahan URL (pemuatan halaman, pushState, rute #hash) sudah terdeteksi; laporkan hanya yang tidak mengubah alamat.

appmintNative.transition appmintNative#

transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>

Contoh

Menjalankan pergantian layar dengan animasi yang terasa native: push menggeser layar baru masuk dari kanan, pop menggeser kembali, fade memudar silang.

Mengembalikan: Promise<void> yang selesai setelah perubahan dan animasinya. update adalah perubahan state Anda (navigate router, setState) dan selalu diterapkan. Membutuhkan: tidak ada.

import { useNavigate } from 'react-router-dom';
import { transition, haptic } from '@/lib/appmintNative';

export function NoteRow({ id, title }: { id: string; title: string }) {
  const navigate = useNavigate();
  return (
    <button
      onClick={() => { haptic('light'); void transition(() => navigate(`/note/${id}`)); }}   // push
    >
      {title}
    </button>
  );
}

export function BackButton() {
  const navigate = useNavigate();
  return <button onClick={() => void transition(() => navigate(-1), { type: 'pop' })}>Back</button>;
}

Memudar di antara dua tab:

import { transition } from '@/lib/appmintNative';

declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });

Gunakan fade untuk pergantian tab, dengan satu handler per tab:

import { transition } from '@/lib/appmintNative';

export function Tabs({ tab, setTab }: { tab: 'home' | 'stats'; setTab: (t: 'home' | 'stats') => void }) {
  return (
    <nav>
      <button onClick={() => void transition(() => setTab('home'), { type: 'fade' })} aria-current={tab === 'home'}>Home</button>
      <button onClick={() => void transition(() => setTab('stats'), { type: 'fade' })} aria-current={tab === 'stats'}>Stats</button>
    </nav>
  );
}

Catatan:

  • Ini memakai View Transitions API jika WebView memilikinya. Jika tidak, ia menerapkan pembaruan dan memutar animasi masuk singkat pada #root (atau anak pertama dari <body>).
  • Jika pengguna meminta sistem untuk mengurangi gerakan, ini hanya menerapkan pembaruan - tanpa animasi.
  • Animasinya sekitar 240 ms.

appmintNative.unlock appmintNative#

unlock(title?, subtitle?) — from '@/lib/appmintNative'

Contoh

Meminta pemilik ponsel membuktikan kehadirannya - sidik jari, wajah, atau PIN/kode sandi perangkat - dan memberi tahu Anda apakah berhasil.

Mengembalikan: Promise<boolean> - true jika terbuka; false jika dibatalkan, gagal, terkunci, atau tidak ada yang bisa memeriksa di perangkat ini. Tidak pernah melempar kesalahan. Membutuhkan: di aplikasi, Fingerprint di Langkah 4 (Akses) saat Anda membuat build. Di web: autentikator platform browser (WebAuthn).

import { unlock, can } from '@/lib/appmintNative';

export async function openPrivateNotes(show: () => void) {
  if (can('unlock') === 'none') {
    alert('This device has no fingerprint, face or PIN check.');
    return;
  }
  const ok = await unlock('Unlock notes', 'Use your fingerprint or PIN');
  if (ok) show();
}

Sembunyikan tombol jika tidak ada yang bisa dipakai untuk membuka kunci:

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

const showLockButton = can('unlock') !== 'none';   // 'native' | 'web' | 'none'

Lindungi penghapusan dengan buka kunci baru, lalu konfirmasi dengan haptic dan toast:

import { unlock, haptic, toast } from '@/lib/appmintNative';

export async function deleteAccountData(wipe: () => Promise<void>) {
  const ok = await unlock('Confirm delete', 'This removes your saved data');
  if (!ok) { haptic('warning'); toast('Not unlocked, nothing was deleted'); return; }
  await wipe();
  haptic('success');
  toast('Data deleted');
}

Catatan: Di aplikasi, PIN, pola, atau kata sandi perangkat diizinkan sebagai cara masuk kedua. Di web, panggilan pertama mendaftarkan autentikator platform browser dan dihitung sebagai terbuka. Ini membuktikan pemilik perangkat hadir; ini BUKAN login - jangan pernah jadikan satu-satunya penjaga data yang ada di server. Di Android 11 ke atas, permintaan terbuka di ponsel mana pun yang punya kunci layar - ponsel dengan PIN saja juga mendapatkannya, dan dibuka dengan PIN; di Android 10 ke bawah, sidik jari harus sudah tersimpan, jika tidak ia menjawab false tanpa permintaan.

appmintNative.updates appmintNative#

updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()

Contoh

Pembaru sisi halaman untuk pembaruan web-bundle. Kode halaman tidak membutuhkannya: di aplikasi AppMint, Remote Update dilakukan secara native oleh aplikasi Android, dan updates.supported() mengembalikan false di mana saja.

Mengembalikan: supported() → boolean (saat ini selalu false); download(url, sha256, version, binaryVersion) → Promise<boolean> (false selama tidak didukung); stage(dir, version, binaryVersion) → Promise<boolean>; confirm() → void; revertIfUnconfirmed(binaryVersion) → Promise<boolean>; current() → { path, version, binaryVersion } | null. Membutuhkan: tidak ada untuk aplikasi Anda - pembaruan diterbitkan dari Remote Update di AppMint.

Satu-satunya penggunaan aman di kode aplikasi adalah memeriksa, dan tidak melakukan apa-apa saat nonaktif:

import { updates } from '@/lib/appmintNative';

if (updates.supported()) {
  // Not reached today: the Android app's own updater owns the whole update lane.
  const staged = await updates.download(
    'https://example.com/bundle-1.2.0.zip',
    'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',  // sha256, required
    '1.2.0',
    '1',
  );
  console.log('update staged:', staged);
}

Tampilkan bundle mana yang sedang berjalan (baris "Tentang" di pengaturan):

import { updates } from '@/lib/appmintNative';

const active = updates.current();          // null = no page-staged bundle (always, today)
const label = active ? `Content ${active.version}` : 'Built-in content';

Tampilkan versi konten yang berjalan di baris pengaturan, dengan label bawaan saat halaman tidak punya bundle bertahap:

import { useMemo } from 'react';
import { updates } from '@/lib/appmintNative';

export function AboutRow() {
  const label = useMemo(() => {
    const c = updates.current();
    return c ? `Content ${c.version} (app ${c.binaryVersion})` : 'Built-in content';
  }, []);
  return <p>{label}</p>;
}

Catatan:

  • Di aplikasi Android, manajer Remote Update milik shell mengunduh, memverifikasi, dan menyajikan pembaruan, serta memutar balik bundel yang gagal dimuat. Halaman disajikan oleh shell itu sendiri, jadi bundel yang disiapkan dari kode halaman tidak akan pernah ditampilkan - itulah sebabnya supported() bernilai false.
  • Di web tidak ada yang perlu diperbarui; halamannya adalah apa pun yang dikirim server.
  • Pembaruan hanya bisa mengubah HTML, CSS, dan JavaScript; fitur native baru tetap memerlukan build aplikasi baru.

appmintNative.watchIdle appmintNative#

import { watchIdle } from '@/lib/appmintNative'

Contoh

Memberi tahu aplikasi saat pengguna menganggur atau mengunci layar, dan saat mereka kembali (can('idle')). Mulai dari ketukan pada pertama kali - permintaan izin membutuhkannya.

Mengembalikan: Promise berisi fungsi berhenti. Callback mendapat { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } di awal dan pada setiap perubahan. Ditolak dengan code 'permission-denied' atau 'unavailable'. Membutuhkan: tidak ada, tanpa izin Android.

import { useRef } from 'react';
import { can, watchIdle } from '@/lib/appmintNative';

export function AwayStatus({ setAway }: { setAway: (away: boolean) => void }) {
  const stop = useRef<(() => void) | null>(null);
  if (can('idle') === 'none') return null;
  return (
    <button onClick={async () => {
      stop.current?.();
      stop.current = await watchIdle((s) => setAway(s.user === 'idle' || s.screen === 'locked'), { thresholdMs: 5 * 60000 });
    }}>Show me as away when idle</button>
  );
}

Tampilkan status menjauh dan berhenti memantau saat dilepas; izin yang ditolak dilaporkan dengan code:

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

export function useIdleWatch() {
  const stop = useRef<(() => void) | null>(null);
  const [state, setState] = useState<'off' | 'active' | 'idle' | 'denied'>('off');
  useEffect(() => () => stop.current?.(), []);
  // Call this from a tap: the first permission request needs a gesture.
  const start = async () => {
    if (can('idle') === 'none') return;
    try {
      stop.current = await watchIdle((s) => setState(s.user === 'idle' || s.screen === 'locked' ? 'idle' : 'active'));
    } catch (e) {
      if ((e as { code?: string }).code === 'permission-denied') setState('denied');
    }
  };
  return { state, start };
}

Jeda timer polling saat pengguna pergi dan segarkan saat mereka kembali:

import { watchIdle } from '@/lib/appmintNative';

export async function pausePollingWhenIdle(refresh: () => void) {
  let timer: ReturnType<typeof setInterval> | undefined = setInterval(refresh, 30000);
  const stop = await watchIdle((s) => {
    const away = s.user === 'idle' || s.screen === 'locked';
    if (away && timer) { clearInterval(timer); timer = undefined; }
    if (!away && !timer) { refresh(); timer = setInterval(refresh, 30000); }
  });
  return () => { stop(); if (timer) clearInterval(timer); };
}

Catatan: ambangnya minimal satu menit (batas bawah dalam spesifikasi).

appmintNative.widget appmintNative#

widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()

Contoh

Mengisi widget layar utama aplikasi - satu kartu dengan judul, nilai besar, keterangan, hingga dua belas baris (masing-masing dengan warna, penanda, dan rute opsional), dan ketukan yang membuka aplikasi di sebuah rute.

Mengembalikan: set(card) → Promise<boolean> (true jika tersimpan); clear() → Promise<void>; placedCount() → Promise<number> (berapa salinan yang ditempatkan pengguna); requestPin() → Promise<boolean> (true jika alur "tambah widget" launcher terbuka). Tidak ada yang ditolak. Membutuhkan: tidak ada di aplikasi Android (widget selalu menjadi bagian build).

Perbarui kartu setelah setiap perubahan yang ingin dilihat pengguna di layar utama:

import { can, widget } from '@/lib/appmintNative';

export async function updateTodayWidget(tasks: { title: string; done: boolean }[]) {
  if (can('widget') === 'none') return;          // on the web
  const open = tasks.filter((t) => !t.done);
  await widget.set({
    title: 'Today',
    value: `${open.length} left`,
    lines: open.slice(0, 6).map((t) => '• ' + t.title),
    route: '/today',                              // a tap opens the app here
  });
}

Agenda yang mengisi widget yang diubah ukurannya, dengan latar transparan dan baris terlambat berwarna merah:

import { widget, type WidgetLine } from '@/lib/appmintNative';

export async function updateAgendaWidget(events: { day: string; time: string; title: string; overdue: boolean }[]) {
  const lines: WidgetLine[] = events.slice(0, 12).map((e) => e.overdue
    ? { text: `Overdue: ${e.title}`, color: '#FF5252', marker: '#FF5252', bold: true, route: `/day/${e.day}` }
    : { text: `${e.time} ${e.title}`, marker: '#29B6F6', route: `/day/${e.day}` });
  await widget.set({
    title: 'Agenda', value: 'Today', caption: `${events.length} events`,
    bg: 'rgba(20, 38, 74, 0.6)', textSize: 'auto', lines, route: '/agenda',
  });
}

Tombol "Tambahkan ke layar utama":

import { can, widget, toast } from '@/lib/appmintNative';

export function AddWidgetButton() {
  if (can('widget') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (!(await widget.requestPin())) toast('Long-press your home screen, tap Widgets, and pick this app');
      }}
    >
      Add widget to home screen
    </button>
  );
}

Kosongkan kartu saat pengguna keluar akun, dan tampilkan berapa banyak salinan di layar utama:

import { useEffect, useState } from 'react';
import { can, widget } from '@/lib/appmintNative';

export function WidgetStatus({ signedIn }: { signedIn: boolean }) {
  const [placed, setPlaced] = useState(0);
  useEffect(() => {
    if (can('widget') === 'none') return;
    if (!signedIn) void widget.clear();
    widget.placedCount().then(setPlaced);
  }, [signedIn]);
  return <p>{placed === 0 ? 'Not on your home screen yet' : `On your home screen (${placed})`}</p>;
}

Catatan:

  • Kartu disimpan di perangkat, bertahan setelah reboot, dan setiap salinan yang ditempatkan disegarkan. placedCount() bernilai 0 berarti belum ada salinan di layar utama.
  • Kolom kartu: title (kosong = nama aplikasi), value (tampil besar di accent, disembunyikan jika kosong), caption (teks kecil di bawah nilai), lines (maks 12; enam selalu tampil, lebih banyak jika widget cukup tinggi; satu baris adalah string atau { text, color?, marker?, bold?, wrap?, route? }), route (default /), textSize ('auto' membesarkan teks sesuai widget selama semua baris masih muat, membungkus baris panjang menjadi dua baris; 'small' | 'normal' | 'large' | 'xl' atau angka 0.6-3), align ('start' | 'center' | 'end'), wrap (baris panjang memakai dua baris), dan bg / ink / accent. Warna default memakai token CSS --bg, --ink, --accent; harus berupa #RRGGBB, #AARRGGBB (alpha DULU: #9914264A adalah 60 % buram), #RGB, rgb()/rgba() atau nama warna - nilai hsl()/oklch() kembali ke warna default. set() mengembalikan false untuk textSize/align yang tidak dikenal atau warna baris yang salah.
  • Ketukan membuka aplikasi dengan appmint-shortcut://<package><route>; bacalah dengan launchUrl() / onDeepLink().
  • requestPin() membutuhkan Android 8+ dan launcher yang mendukungnya. clear() menampilkan kartu kosong "Belum ada yang ditampilkan".
  • Hanya untuk aplikasi Android: can('widget') bernilai 'none' di web, tempat set() dan requestPin() mengembalikan false.

Dibuat dari runtime aplikasi dan file contohnya pada setiap build dokumentasi. Baca sebagai Markdown · Semua keluarga.

Diperiksa terhadap jembatan yang dirilis pada 2026-10-22.