API del bridge JavaScript
appmintNative (app create con l'AI)
I wrapper tipizzati che ogni progetto creato con l'AI importa da @/lib/appmintNative. Ognuno sceglie il percorso nativo nell'app e un percorso web, o un onesto none, in un browser: una sola chiamata funziona ovunque.
appmintNative.actionSheet appmintNative#
actionSheet(title: string, options: { title: string; destructive?: boolean }[]): Promise<number | null>
Esempio
Mostra un pannello dal basso «scegli una di queste» - il controllo nativo per il menu delle azioni di una riga.
Restituisce: Promise<number | null> - l'indice dell'opzione scelta, oppure null quando l'utente ha chiuso il pannello (tocco sullo sfondo, Esc, annulla di sistema). La chiusura è normale, non un errore. Richiede: niente.
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); }
}Aprilo da un pulsante visibile e da una pressione prolungata sulla riga:
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>;
}La forma stile Capacitor ActionSheet.showActions({ title?, options }) accetta le stesse opzioni e restituisce { index }, con -1 per un annullamento:
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)`Note:
- Nell'app Android il plugin ActionSheet di Capacitor disegna il foglio di sistema;
destructiveusa il suo stile distruttivo. - Altrove è un vero pannello con la palette dell'app (
--surface,--ink,--danger), con sfondo, Esc, focus portato nel pannello e un tocco di selezione. - Un elenco
optionsvuoto si risolve subito connull. Per qualsiasi contenuto diverso da un elenco di azioni, usasheet().
appmintNative.ActionSheet appmintNative#
import { ActionSheet } from '@/lib/appmintNative'
Esempio
Documentato insieme a appmintNative.actionSheet - l'esempio lì mostra anche questo.
Mostra un pannello dal basso «scegli una di queste» - il controllo nativo per il menu delle azioni di una riga.
Restituisce: Promise<number | null> - l'indice dell'opzione scelta, oppure null quando l'utente ha chiuso il pannello (tocco sullo sfondo, Esc, annulla di sistema). La chiusura è normale, non un errore. Richiede: niente.
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); }
}Aprilo da un pulsante visibile e da una pressione prolungata sulla riga:
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>;
}La forma stile Capacitor ActionSheet.showActions({ title?, options }) accetta le stesse opzioni e restituisce { index }, con -1 per un annullamento:
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)`Note:
- Nell'app Android il plugin ActionSheet di Capacitor disegna il foglio di sistema;
destructiveusa il suo stile distruttivo. - Altrove è un vero pannello con la palette dell'app (
--surface,--ink,--danger), con sfondo, Esc, focus portato nel pannello e un tocco di selezione. - Un elenco
optionsvuoto si risolve subito connull. Per qualsiasi contenuto diverso da un elenco di azioni, usasheet().
appmintNative.addContact appmintNative#
addContact(contact: Contact): Promise<boolean>
Esempio
Salva un nuovo contatto nella rubrica del telefono. Solo app Android: il bridge della shell lo salva direttamente (non si apre alcuna schermata Contatti).
Restituisce: Promise<boolean> - true una volta salvato, false quando l'utente ha rifiutato l'autorizzazione ai contatti o il salvataggio è fallito. Viene rifiutata con un Error il cui code è 'not_enabled' (la build non ha attivato la scrittura dei contatti) o 'foreign_origin', e dopo 5 minuti di attesa senza risposta. Richiede: l'interruttore Modifica contatti nel Passaggio 4 (Accesso), che la build attiva quando il tuo codice la chiama, e l'autorizzazione dell'utente al primo utilizzo.
Salva un cliente da un modulo.
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
}
}Salta un contatto già salvato. searchContacts legge la rubrica, quindi controlla prima di aggiungere:
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';
}Distingui un errore di build da un rifiuto. Un rifiuto con code 'foreign_origin' o 'not_enabled' è un problema della build, false è una scelta dell'utente:
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>;
}Note: Salva subito, senza aprire alcun editor di contatti, quindi chiedi prima conferma all'utente nella tua schermata. Salva i numeri di telefono come cellulari e il contatto sul dispositivo (nessun account). La scrittura dei contatti è delicata: dichiarala nel modulo Sicurezza dei dati di Play e nell'informativa sulla privacy.
appmintNative.alarmSupport appmintNative#
alarmSupport() — @/lib/appmintNative
Esempio
Indica se scheduleAlarm può impostare una sveglia in questo momento e, in caso contrario, cosa manca.
Restituisce: 'ready', 'not_enabled' (creata senza Sveglia), 'needs_exact_alarm' (Android 12+: le sveglie esatte sono disattivate), 'needs_notification_permission' (Android 13+) oppure 'unavailable' (web e iOS: non mostrare lì i controlli della sveglia). Richiede: 🔔 Sveglia nel passaggio Accesso (Passaggio 4) quando crei l'app.
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>;
}Imposta una sveglia giornaliera solo quando il telefono è pronto e di' perché quando non lo è:
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.';
}Nascondi la schermata della sveglia sul web e reagisci quando una sveglia termina, combinandolo con 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)
Esempio
Il nome, l'id del pacchetto e la versione dell'app, un modo per chiuderla e gli eventi dell'app, nella forma del plugin App di Capacitor.
Restituisce: getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → la forma di Capacitor (una Promise dell'handle che ha anche remove()). Eventi: appStateChange ({ isActive }), pause, resume (la pagina che va in background / ne torna), appUrlOpen ({ url }, un deep link, come onDeepLink), backButton (la pressione o il gesto indietro; decide il tuo gestore: l'app non si chiude anche da sola). Richiede: nulla.
import { App } from '@/lib/appmintNative';
const info = await App.getInfo();
aboutLine.textContent = `${info.name} ${info.version}${info.build ? ` (${info.build})` : ''}`;Chiudi l'app da una voce "Esci" che non sia l'unico modo per proseguire:
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();
}Mostra la versione dell'app in una schermata 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>;
}Note: Nell'app getInfo legge il plugin App di Capacitor; quando manca usa nome e versione del bridge della shell (id e build sono allora ''), e sul web document.title con una versione vuota. exitApp usa closeWindow della shell nell'app e non fa nulla sul web, quindi non rendere mai Esci l'unico modo per andare avanti. Per gli eventi di pausa/ripresa e i deep link vedi Capacitor.Plugins.App, launchUrl() e onDeepLink().
appmintNative.appLauncher appmintNative#
appLauncher.canOpen(url): Promise<boolean> / appLauncher.open(url): Promise<boolean>
Esempio
Apre un'altra app tramite link - whatsapp://send?text=…, tel:, mailto:, geo:, o un link https che il sistema operativo passa alla sua app - e chiede prima se può funzionare.
Restituisce: canOpen() → Promise<boolean>. open() → Promise<boolean> (false quando nessuna app ha preso il link). Richiede: niente.
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);
}Chiama o scrivi un'email (non serve alcun controllo; un telefono ha sempre un'app per queste azioni):
import { appLauncher } from '@/lib/appmintNative';
await appLauncher.open('tel:+15551234567');
await appLauncher.open('mailto:[email protected]?subject=Order%2042');Apri l'app delle mappe con un link geo: e dillo quando nessuna app l'ha preso:
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.');
}Note: Nell'app usa il plugin Capacitor AppLauncher. Su Android 11+ canOpen vede solo gli schemi che l'app dichiara (https, geo, google.navigation, tel, mailto, sms, whatsapp, intent, upi); per gli altri risponde false anche se l'app è installata: chiama open() e controlla invece il suo risultato. Sul web uno schema personalizzato non si può verificare, quindi canOpen è true solo per http(s), e open apre il link.
appmintNative.appLock appmintNative#
appLock({ timeoutSec?, title?, onLocked? }) — from '@/lib/appmintNative'
Esempio
Blocca di nuovo l'app quando l'utente torna dopo che è rimasta un po' in background: chiede impronta / volto / PIN (unlock()) e attiva la schermata di privacy, così il selettore delle app non mostra i tuoi contenuti.
Restituisce: una funzione che ferma il blocco (chiamala all'uscita dall'account o allo smontaggio). onLocked(true) scatta quando l'app si blocca, onLocked(false) dopo uno sblocco riuscito. Richiede: lo stesso di unlock() - Android: Impronta digitale spuntata quando crei l'app.
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;
}Nel componente radice: const locked = useAppLock(); if (locked) return <LockScreen />;.
Ferma il blocco all'uscita dall'account, così a un utente disconnesso non viene chiesto di sbloccare:
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;
}Offri il blocco solo dove il dispositivo può sbloccarlo e abbinalo a unlock() per un pulsante manuale "Blocca ora":
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>
</>
);
}Note: Se il dispositivo non ha nulla con cui sbloccare (can('unlock') === 'none') non fa nulla e restituisce una funzione vuota - un blocco dell'app che non potrebbe mai aprirsi bloccherebbe l'utente. Se l'utente annulla, chiede di nuovo fino a cinque volte, poi resta bloccato (tieni visibile la tua schermata di blocco) finché l'app non va in background e torna. È separato dall'opzione Blocco con PIN nella procedura guidata di build, che l'app mostra in modo nativo all'avvio.
appmintNative.appUpdate appmintNative#
appUpdate.check(): Promise<AppUpdateInfo | null> / appUpdate.start(): Promise<boolean>
Esempio
Controlla su Google Play se c'è una versione più recente e avvia l'aggiornamento in-app di Play (scarica e installa sul posto).
Restituisce: check() → Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null sul web, senza Play services o quando Play fallisce). updateAvailable è la risposta dello store stesso. start() → Promise<boolean>: true solo quando l'aggiornamento di Play è stato completato, false quando l'utente ha annullato, è fallito o non ce n'era uno disponibile. Richiede: niente; l'app deve essere installata da 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);
});
}Confronta il version code di Play con quello installato e tratta null come «non si può controllare qui»:
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}).`;
}Un banner React che controlla una volta quando la schermata si apre:
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>;
}Note: updateAvailable è la risposta di Play stesso; inPlace aggiunge che Play può eseguire subito il suo aggiornamento in-app. availableVersion è il CODICE di versione di Play (Play non fornisce un nome): confrontalo con currentVersionCode, mai con currentVersion, che è il nome della versione.
appmintNative.askForReview appmintNative#
askForReview(options): Promise<ReviewOutcome>
Esempio
Chiede a Google Play di mostrare la sua scheda di valutazione dentro l'app e riferisce cosa è successo.
Restituisce: Promise<ReviewOutcome> - { result, openedStore, message? } con result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' sul web). { openStore: true } apre la scheda su Play quando la finestra non è comparsa. Richiede: nulla; la finestra compare solo per un'app installata da 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!');
}Di' all'utente cosa è successo per ogni esito:
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}` : ''}.`;
}
}Chiedi una sola volta, dopo che l'utente ha finito la sua terza attività, e ricorda di averlo chiesto:
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();
}Note: Chiedi dopo un momento di successo e raramente. Un'installazione di prova fuori da Play risponde sempre not_from_play.
appmintNative.backgroundLocation appmintNative#
import { backgroundLocation } from '@/lib/appmintNative'
Esempio
Una posizione che continua ad arrivare mentre l'app è in background: start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.
Restituisce: start(options) → Promise<BgResult> ({ ok, background, backgroundPermission, precise } o { ok: false, reason }). stop / clearBuffered → Promise<boolean>. buffered() → Promise<LocationFix[]>. requestAlways() → Promise<{ foreground, background, precise, error? }>. onFix(cb) → un annullamento dell'iscrizione; cb riceve ogni dettaglio appmint:location. Richiede: Posizione in background nella build; autorizzazione alla posizione (chiedila prima con navigator.geolocation).
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(); };
}Chiedi «Consenti sempre» prima di iniziare e spiega un rifiuto:
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}).`;
}Un hook React che mostra i punti in tempo reale e smette di ascoltare quando la schermata si chiude:
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'
Esempio
Richieste HTTP native che funzionano mentre l'app è chiusa: enqueue (un salvataggio automatico una tantum e duraturo, inviato quando sei online), schedule (una richiesta ripetuta, al massimo ogni 15 minuti), cancel e list.
Restituisce: enqueue / schedule → Promise<boolean>; cancel → Promise<void>; list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } - pendingDelivery su Android: una singola esecuzione non ancora consegnata). Richiede: nulla nell'app Android (usa WorkManager). Sul web can('backgroundSync') è 'none' e enqueue / schedule si risolvono false.
Salvataggio automatico durevole: lo stesso id sostituisce il salvataggio non ancora inviato.
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),
});
}Sincronizzazione ripetuta, elenco e annullamento:
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');Evento grezzo (ciò che il wrapper attende su 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);
});Note: true significa che l'attività è stata messa in coda, non consegnata. false significa che non è stata messa in coda: la shell ha risposto status: 'failed' (un URL che non inizia con http, un id vuoto, o il sistema ha rifiutato) oppure non ha risposto entro 10 secondi. Il JavaScript della pagina non viene mai eseguito in background; solo questa richiesta lo è. Una risposta 4xx non viene ritentata; gli errori di rete e 5xx vengono ritentati fino a 8 volte.
appmintNative.backgroundTasks appmintNative#
import { backgroundTasks } from '@/lib/appmintNative'
Esempio
Il codice della pagina, eseguito periodicamente con l'app chiusa (wrapper tipizzato su AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.
Restituisce: register / runNow → Promise<BgResult>; unregister / done → Promise<boolean>; list() → le attività registrate con lastRunAt / lastResult; isBackgroundRun() / currentRun() sono sincrone. Richiede: Esegui il codice della pagina da chiusa nella 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();
}Registrati all'accesso, rimuovi all'uscita e elenca ciò che è registrato:
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.';
}Esegui un'attività subito da un pulsante "Sincronizza ora" e controlla la risposta:
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'
Esempio
Imposta o cancella il conteggio dei non letti sull'icona dell'app nel launcher.
Restituisce: badge.set(count) → Promise<boolean> (false quando il plugin Badge non c'è, ad esempio sul web); badge.clear() → Promise<void>. Richiede: nulla su Android (usa il plugin Badge integrato).
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();
}Rispecchia il conteggio dei non letti quando arriva un messaggio push mentre l'app è aperta:
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);
}Segna tutto come letto e azzera il conteggio insieme; salta il badge dove non esiste:
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;
}Note: su Android l'aspetto dipende dal launcher: molti mostrano un numero, alcuni solo un punto, alcuni niente - non mettere informazioni solo nel badge. Sul web can('badge') è 'none'.
appmintNative.barcodeFormats appmintNative#
barcodeFormats(): Promise<string[]>
Esempio
Elenca i formati di codici a barre e QR che il BarcodeDetector di questo dispositivo può leggere.
Restituisce: Promise<string[]> - nomi di formato come 'qr_code', 'ean_13', 'code_128'; un array vuoto quando la lettura dei codici a barre non è disponibile qui. Non viene mai rifiutata. Richiede: niente.
Mostra l'opzione «Scansiona il codice a barre di un prodotto» solo quando il dispositivo legge quel formato:
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;
}Limita una scansione a ciò che il dispositivo supporta:
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)));Crea un selettore di formati che elenchi solo ciò che questo dispositivo può leggere:
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>
);
}Note:
- Interroga
BarcodeDetector.getSupportedFormats()del motore del browser. La WebView dell'app Android lo supporta (misurato: aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e); molti browser desktop e Safari no, e rispondono[]. - Questo elenco riguarda
readBarcodeFrom()/scanBarcode().scanCode()nell'app usa invece lo scanner di Google e non dipende da esso.
appmintNative.batteryOptimization appmintNative#
import { batteryOptimization } from '@/lib/appmintNative'
Esempio
L'esenzione dall'ottimizzazione della batteria: isIgnoring() e request().
Restituisce: isIgnoring() → booleano. request() → Promise<string>: 'already_ignoring', 'dialog_opened' (opzione di build "Chiedi di saltare direttamente l'ottimizzazione batteria"), 'settings_opened', 'unavailable', oppure 'unsupported' fuori dall'app Android. Richiede: niente per l'elenco delle impostazioni.
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;
}Mostra lo stato attuale in una riga delle impostazioni:
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>
);
}Chiedi l'esenzione prima di avviare un lungo lavoro in background, insieme a 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'
Esempio
Sensori fitness Bluetooth standard - frequenza cardiaca, velocità/cadenza della bici, potenza, velocità/cadenza della corsa e batteria del sensore - con i numeri già elaborati, nell'app Android.
Restituisce: ogni metodo restituisce Promise<void> (setWheelCircumference restituisce void). Tutto il resto arriva come eventi della finestra appmint:ble con detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. Richiede: Bluetooth, che la build attiva da sola quando il codice usa bleSensors. In un browser (anteprima web) non c'è un bridge dei sensori: startScan() risponde con {kind:'error', error:'not_enabled'}, quindi gestisci questo errore. Per saperlo in anticipo, controlla can('bluetooth') === 'native' - non !== 'none': in Chrome can('bluetooth') è 'web' (Web Bluetooth), che bleSensors non usa.
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>
);
}Limita la schermata all'app installata.
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>;
}Ferma la scansione al primo sensore e osserva la velocità di pedalata.
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>;
}Note: Le scansioni durano 15 secondi (stopScan() ne termina una in anticipo); si collega un solo sensore alla volta. Codici di errore: not_enabled, permission_denied, bluetooth_unavailable, bluetooth_off, scan_failed, invalid_address. Per la velocità servono due pacchetti e la misura della ruota (predefinita 2096 mm). Gli stessi messaggi vanno anche a window.onAppMintBle; ascoltare l'evento è più semplice. Per qualsiasi altro dispositivo Bluetooth usa navigator.bluetooth.
appmintNative.brightness appmintNative#
brightness.get() / brightness.set(value) / brightness.restore()
Esempio
Alza la luminosità dello schermo per una carta d'imbarco o un codice QR, poi restituisce il controllo al sistema. Solo nativo: una pagina web non può controllare la retroilluminazione.
Restituisce: get() → Promise<number | null> (0-1 mentre l'app forza il livello; -1 quando l'app segue il livello di sistema; null sul web o quando non si può leggere). set(value) → Promise<boolean> (valore limitato a 0-1; false sul web). restore() → Promise<void>. Richiede: nulla.
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 />;
}Ricorda il livello, aumenta la luminosità, poi ripristinalo.
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;
}Tieni lo schermo luminoso e acceso mentre un codice è visibile.
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]);
}Note: Cambia solo la finestra di questa app, non l'impostazione di luminosità del telefono, e la modifica termina alla chiusura dell'app. restore() imposta -1 (segue il sistema). get() legge la modifica della finestra, quindi è -1 finché non chiami set() - non è il livello di sistema.
appmintNative.browser appmintNative#
browser.open(url, { toolbarColor? }) / browser.close() / browser.onClosed(cb)
Esempio
Apre una pagina in una scheda del browser integrato nell'app (Chrome Custom Tabs), per termini, pagine di aiuto o una pagina di accesso, e una nuova scheda sul web.
Restituisce: open() → Promise<boolean> (false quando la scheda non si è potuta aprire). close() → Promise<void>. onClosed(cb) → una funzione per annullare l'iscrizione; cb viene eseguita quando l'utente chiude la scheda (solo nell'app). Richiede: niente.
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.'); }
}Un link ai termini in un componente 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>
);
}Chiudi la scheda dal codice e aggiorna quando l'utente torna.
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
};
}Note: toolbarColor usa per impostazione predefinita il token --bg dell'app. La scheda è un browser separato: non condivide i cookie né localStorage dell'app. close() chiude la scheda dal codice nell'app; sul web non fa nulla. onClosed sul web non scatta mai.
appmintNative.calendar appmintNative#
calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar
Esempio
Aggiunge, elenca ed elimina eventi nel calendario PERSONALE del telefono dell'utente (il provider calendario di Android) e apre l'app calendario a una data.
Restituisce: requestPermission() → Promise<'granted' | 'denied'>. createEvent(input) → Promise<string | null> (l'ID dell'evento; null quando l'utente ha negato l'accesso al calendario - viene rifiutata se l'evento stesso non si è potuto creare, ad esempio per date non valide). listEvents({ from, to }) → Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (orari in millisecondi epoch). deleteEvent(id) / openCalendar(date?) → Promise<boolean>. Solo createEvent viene rifiutata, e solo per un evento che non si è potuto creare. Richiede: attiva Calendario nel Passaggio 4 (Accesso) quando crei l'app - la build lo spunta per te quando vede calendar.createEvent(, calendar.listEvents( o calendar.requestPermission( nel codice.
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);
}Elenca questa settimana, elimina, apri:
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());Aggiungi al calendario solo quando la scrittura è possibile.
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;
}Note: Sul web createEvent scarica un file .ics che l'utente apre nel proprio calendario (restituisce il suo UID), listEvents restituisce [] (can('calendarRead') è 'none'), e deleteEvent / openCalendar restituiscono false. Promemoria ed eventi ripetuti sono disponibili su Capacitor.Plugins.Calendar, non tramite questo wrapper.
appmintNative.camera appmintNative#
camera.take(opts?) / camera.pick(opts?)
Esempio
Scatta una foto con la fotocamera o sceglie foto dalla galleria: il plugin Capacitor Camera nell'app, un campo file con capture sul web.
Restituisce: take() → Promise<Photo | null> con { dataUrl, mime }; null se annullato o se l'autorizzazione è rifiutata. pick() → Promise<Photo[]>, vuoto se annullato. Richiede: Camera nel Passaggio 4 (Accesso) - la build lo attiva quando il tuo codice richiama camera.take.
Scatta una foto del profilo con la fotocamera frontale.
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;
}Scegli una foto dalla galleria con take, o più foto con 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);Carica una foto trasformando il data URL in un 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 });
}Note: Opzioni: source ('camera' predefinito, oppure 'photos'), quality 0-100 (predefinito 80), direction ('rear' predefinito, oppure 'front'), allowEditing. Le foto non vengono salvate nella galleria. Un'autorizzazione rifiutata si risolve con null / [], senza mai lanciare errori: mostra un breve suggerimento perché l'utente capisca perché non è successo nulla. La fotocamera è un dato sensibile: dichiarala nel modulo Sicurezza dei dati di Play.
appmintNative.can appmintNative#
can(cap: Capability): 'native' | 'web' | 'none'
Esempio
Ti dice dove una funzione girerà davvero sulla piattaforma attuale, così l'app mostra solo controlli che funzionano.
Restituisce: una stringa, in modo sincrono: 'native' (l'implementazione propria dell'app: il bridge della shell o un plugin Capacitor incluso nell'app), 'web' (un vero standard web in questo browser) oppure 'none' (non disponibile qui: non mostrare il controllo). Richiede: nulla.
Nascondi un controllo che non può funzionare su questa piattaforma:
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>;
}Cambia la formulazione in base alla piattaforma:
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';Nomi delle funzionalità: 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.
Mostra un suggerimento al posto di un controllo inattivo e trasforma una funzione in un semplice sì/no con 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';Note:
- Un plugin Capacitor incluso nell'app risponde sempre
'native'; altrimenti si controlla il bridge della shell, poi lo standard web. toast,actionSheet,dialog,statusBareswipehanno una vera versione web, quindi non sono'none'in un browser.contactList,contactAdd,smsSend,smsList,callLog,shortcuts,shareTargetewidgetsono'none'sul web.- Alcune funzioni esistono solo quando la build le ha attivate (accesso alle cartelle, chiavi sicure, controlli multimediali, SMS, contatti). Chiedi a
can(), nonisNativeApp(). - Un nome sconosciuto restituisce
'none'. Per un semplice sì/no usahas(cap).
appmintNative.clearPushInbox appmintNative#
import { clearPushInbox } from '@/lib/appmintNative'
Esempio
Svuota la posta in arrivo di AppMint Push su questo telefono. Permanente - non si può annullare.
Restituisce: boolean in modo sincrono - true quando svuotato; sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
import { clearPushInbox, pushInbox } from '@/lib/appmintNative';
export async function onClearAll(confirmDelete: () => Promise<boolean>) {
if (!(await confirmDelete())) return pushInbox();
clearPushInbox();
return pushInbox(); // []
}Mostra Cancella solo quando c'è qualcosa da cancellare e riporta il risultato:
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>
);
}Segna tutto come letto invece di eliminarlo, così non si perde nulla:
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;
}Note: le notifiche nel vassoio e la cronologia degli invii in AppMint non vengono toccate; i nuovi messaggi arrivano comunque.
appmintNative.click appmintNative#
click() — @/lib/appmintNative
Esempio
Riproduce una volta il suono al tocco dell'app, per i tuoi pulsanti.
Restituisce: void. Richiede: Suono al tocco attivato nel Passaggio 3 (Integra) quando crei l'app. Sul web, e nelle app create senza Suono al tocco, non fa nulla.
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>
);
}Lascia che l'utente disattivi l'audio nelle tue impostazioni:
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
}Riproducilo da una riga di lista insieme a un 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>;
}Note: Con Segui l'impostazione del telefono (consigliato) il clic è silenzioso quando l'utente ha disattivato i "Suoni al tocco" sul telefono. Con Riproduci sempre a ogni tocco, ogni tocco fa già il clic, quindi non servono chiamate click() aggiuntive. Solo Android (la WebToApk.playClick).
appmintNative.Clipboard appmintNative#
Clipboard.write({ string?, url? }) / Clipboard.read(): Promise<{ value, type }>
Esempio
Gli appunti nella forma del plugin Clipboard di Capacitor: lo stesso codice di copy() / paste().
Restituisce: write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value è '' quando gli appunti sono vuoti). Richiede: nulla.
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
}
}Copia un codice e conferma con un toast:
import { Clipboard, toast } from '@/lib/appmintNative';
export async function copyCode(code: string) {
await Clipboard.write({ string: code });
toast('Code copied');
}Scrivi un link quando non c'è testo e tratta degli appunti vuoti come niente da incollare:
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
}
}Note: Nell'app usa il plugin Clipboard di Capacitor; in un browser navigator.clipboard. write invia solo string (oppure url quando string è vuoto), senza immagini. In un browser read() può lanciare un errore quando l'utente nega l'accesso; nell'app degli appunti vuoti si ottiene '', non un errore. Chiedi a can('clipboardRead') prima di mostrare un controllo Incolla.
appmintNative.collapsingHeader appmintNative#
collapsingHeader(header: HTMLElement, opts?: { scroller?: HTMLElement | Window; range?: number }): () => void
Esempio
Fa reagire un'intestazione con titolo grande allo scorrimento come quelle native, pubblicando un valore da 0 a 1 che il tuo CSS può usare.
Restituisce: una funzione per annullare l'iscrizione che rimuove anche la variabile e l'attributo. Richiede: nulla.
Imposta --collapse (da 0 in alto a 1 dopo range px di scorrimento) sull'intestazione e aggiunge data-collapsed quando è completamente compressa:
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); }Smetti di ascoltare da uno script semplice; questo rimuove anche --collapse e 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);Note:
- Il valore predefinito di
scrollerè la finestra (una pagina che scorre nel suo insieme); il valore predefinito dirangeè 64 px. - Il listener di scorrimento è passivo e si aggiorna una volta per fotogramma dell'animazione, quindi non rallenta lo scorrimento.
appmintNative.composeSms appmintNative#
composeSms(phoneNumber: string, message: string): boolean
Esempio
Apre l'app di messaggistica con numero e testo già compilati; l'utente preme invia. Funziona nell'app Android (bridge della shell) e sul web (un link sms:).
Restituisce: boolean subito - true quando è stata richiesta l'apertura dell'app di messaggistica. Richiede: nessuna autorizzazione.
Un pulsante «Scrivi al negozio».
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.');
});
}Un hook riutilizzabile che nasconde il pulsante quando non c'è un numero.
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}.`),
};
}Invita un amico; l'utente preme invio.
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');
}Note: Preferiscilo a sendSms() per tutto ciò che l'utente scrive o approva: nulla lascia il telefono senza che l'utente prema invia. Sul web apre sms:…?body=…. Una chiamata composeSms( non attiva SMS (leggi e invia) quando crei l'app, quindi non aggiunge alcuna autorizzazione limitata da Play.
appmintNative.copy appmintNative#
copy(text: string): Promise<boolean>
Esempio
Mette del testo negli appunti di sistema.
Restituisce: Promise<boolean> - true quando il testo è stato copiato, false quando gli appunti non sono disponibili o il browser ha rifiutato. Richiede: niente.
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>
);
}La forma stile Capacitor fa la stessa cosa:
import { Clipboard } from '@/lib/appmintNative';
await Clipboard.write({ string: 'ABC-123' });Segnala entrambi gli esiti, con una vibrazione solo in caso di successo:
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;
}Note: Nell'app il plugin Capacitor Clipboard scrive il testo (un errore del plugin rifiuta la promise invece di restituire false). In un browser usa navigator.clipboard.writeText, che richiede una pagina https e di solito un tocco dell'utente.
appmintNative.copyImage appmintNative#
import { copyImage } from '@/lib/appmintNative'
Esempio
Copia un'immagine negli appunti così si incolla nelle app di chat e di posta: gli appunti dell'app in Android, l'Async Clipboard API sul web.
Restituisce: Promise<boolean> - true una volta copiato. Richiede: nulla; condiziona il controllo a 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');
}Copia una foto dalla fotocamera, poi confermala con un 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');
}Mostra il pulsante solo dove gli appunti accettano immagini e copia un'immagine da un 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>
);
}Note: PNG è il tipo più sicuro ovunque; l'app Android accetta anche JPEG e WebP.
appmintNative.Device appmintNative#
Device.getInfo() / Device.getId() / Device.getLanguageCode()
Esempio
La forma di @capacitor/device: modello del telefono, versione del sistema, un ID stabile e la lingua. Usa il plugin Device di Capacitor nell'app e la risposta del browser sul web.
Restituisce: getInfo() → Promise<DeviceInfo & { operatingSystem, isVirtual }> (con Capacitor anche manufacturer, webViewVersion, …); getId() → Promise<{ identifier }>; getLanguageCode() → Promise<{ value }> (due lettere, ad esempio "en"). Richiede: nulla.
Mostra il modello e la versione del sistema operativo in una schermata Informazioni.
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' : '');
}Un ID stabile e il codice lingua.
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 }),
});Mostra la versione dell'app e il modello del dispositivo nel piè di pagina delle impostazioni. Il report di sincronizzazione fornisce la versione dell'app; il plugin aggiunge il sistema operativo.
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}`;
}Note: Nell'app la sincronizzazione deviceInfo() compila anche model e osVersion; Device.getInfo() aggiunge operatingSystem, isVirtual e i campi extra del plugin. Sul web operatingSystem è 'unknown' e model è vuoto. getId() sul web è l'ID localStorage di installId() e genera un errore quando l'archiviazione è bloccata.
appmintNative.deviceInfo appmintNative#
deviceInfo(): DeviceInfo
Esempio
Ti dice in modo sincrono dove gira l'app (platform) e la lingua, più il report completo del dispositivo della shell Android dentro l'app Android.
Restituisce: DeviceInfo subito: sempre { platform, language } - platform è 'android' nell'app e 'web' in un browser. Nell'app Android compila anche model, manufacturer, osVersion (es. "14") e appVersion (es. "1.4.0"), e include ogni chiave di WebToApk.getDeviceInfo(): android, hardware, screen, app, runtime, installId. Sul web aggiunge osVersion (lo user agent). Richiede: niente.
Ramifica in base alla piattaforma durante il rendering.
import { deviceInfo } from '@/lib/appmintNative';
const info = deviceInfo();
const isAndroidApp = info.platform === 'android';
const lang = info.language; // e.g. "en-IN"Leggi i dettagli di Android. I campi comuni sono in alto; il resto del report della shell resta nei suoi gruppi annidati.
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}%`);
}Scegli il controllo giusto in base alla piattaforma.
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>;
}Note: model e appVersion sono compilati solo nell'app. Sul web osVersion è la stringa user-agent del browser, non una versione del sistema operativo. Genera un errore se la risposta della shell non è JSON valido (un vero difetto, mai previsto).
appmintNative.Dialog appmintNative#
Dialog.alert / Dialog.confirm / Dialog.prompt
Esempio
Veri dialoghi di sistema: un messaggio, una domanda sì/no, una domanda di testo a una riga. L'app usa il plugin Dialog di Capacitor; il web usa il alert / confirm / prompt.
Restituisce: alert({ title?, message }) → Promise<void>. confirm({ title?, message, okButtonTitle?, cancelButtonTitle? }) → Promise<{ value: boolean }>. prompt({ title?, message, inputPlaceholder?, inputText? }) → Promise<{ value: string; cancelled: boolean }>. confirm e prompt non vengono mai rifiutate (la chiusura è una risposta); alert viene rifiutata solo quando non può essere mostrata (nessun messaggio). Richiede: niente.
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);
}Avviso e richiesta:
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());Chiedi conferma prima di uscire dall'account. Chiudere la finestra di dialogo conta come "no".
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');
}Note: Chiudere una conferma restituisce { value: false }; chiudere una richiesta di testo restituisce { value: '', cancelled: true }. Le finestre di dialogo bloccano la schermata: usale per decisioni vere, e toast() per messaggi tipo "Salvato". prompt non passa al plugin titoli personalizzati per i pulsanti.
appmintNative.dismissPushMessage appmintNative#
import { dismissPushMessage } from '@/lib/appmintNative'
Esempio
Nasconde un messaggio dalla posta in arrivo di AppMint Push (scorrimento per rimuoverlo). Annullalo con restorePushMessage(id).
Restituisce: boolean in modo sincrono - true se era nascosto. Sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app.
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());
});
}Mostra la posta in arrivo con un gestore dello scorrimento e uno stato di annullamento, così uno scorrimento non è mai definitivo.
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>
);
}Nascondi in una volta tutti i messaggi letti e indica quanti sono stati nascosti.
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;
}Note: cambia solo la posta in arrivo di questo telefono; la notifica nella barra e la cronologia degli invii restano.
appmintNative.docScanner appmintNative#
docScanner.scan({ pageLimit?, pdf?, quality? }): Promise<{ pages, pdfPath? } | null> / docScanner.isSupported(): Promise<boolean>
Esempio
Apre lo scanner di documenti del sistema - rilevamento dei bordi, raddrizzamento, più pagine - lo scanner ML Kit di Google nell'app Android. Le pagine tornano come URL di dati JPEG.
Restituisce: Promise<{ pages: string[]; pdfPath?: string } | null> - null quando l'utente annulla, lo scanner non può partire, o sul web. isSupported() → Promise<boolean>: se questo telefono può scansionare (servizi Google Play); false sul web. Richiede: niente da attivare (su Android lo scanner gira nei servizi Google Play e non richiede l'autorizzazione della fotocamera).
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>
);
}Chiedi prima al telefono, perché un dispositivo più vecchio potrebbe non avere lo scanner, poi conserva le pagine.
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} />)}
</>
);
}Scansiona, poi carica la prima pagina con l'helper per il trasferimento di 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;
}Note: pageLimit 0 (predefinito) = nessun limite; quality 0,1-1 (predefinito 0,85). Con pdf: true, pdfPath è un percorso di file nella cache dell'app, non un URL che la pagina può caricare. I data URL sono grandi: ridimensionali o caricali invece di tenerne molti in localStorage. Per una semplice foto usa camera.take().
appmintNative.enableNotifications appmintNative#
enableNotifications(): Promise<EnableNotificationsResult>
Esempio
Riporta un utente che ha negato o saltato le notifiche, da un pulsante che ha toccato: la finestra di autorizzazione finché può ancora essere mostrata, altrimenti (app Android) le impostazioni delle notifiche di questa app.
Restituisce: Promise<{ state, changed, openedSettings }> - state è 'granted' | 'prompt' | 'blocked' | 'unavailable', changed è true quando differisce da prima della chiamata, openedSettings è true quando è stata usata la schermata delle impostazioni. Non viene mai rifiutata. Sul web chiede tramite Notification.requestPermission(). Richiede: le notifiche nella build (sono attive ogni volta che l'app invia o riceve notifiche).
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';
});
}Note: mostra il banner solo per 'prompt' e 'blocked'; nascondilo per 'granted' e 'unavailable'. Chiedi da un tocco, mai all'avvio.
appmintNative.fileTransfer appmintNative#
fileTransfer.download / fileTransfer.upload from '@/lib/appmintNative'
Esempio
Scarica o carica un file con una funzione di avanzamento. Nell'app il trasferimento avviene in modo nativo (Capacitor FileTransfer); sul web usa fetch / XMLHttpRequest con la stessa funzione.
Restituisce: download(url, fileName, opts?) → Promise<{ path?: string; blob?: Blob }> - path (un file nella cache dell'app) nell'app, blob sul web (dove il file viene salvato anche tramite il browser). upload(url, file, opts?) → Promise<{ responseCode: number; responseBody: string }>. upload si risolve con qualunque stato HTTP abbia risposto il server (controlla responseCode), nell'app come sul web; entrambe vengono rifiutate quando il trasferimento stesso fallisce (nessuna connessione, file non valido). Richiede: niente da attivare.
Scarica con una barra di avanzamento:
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)}`);
}
}Carica un File da un <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})`);
}Scarica con uno stato di avanzamento, poi apri il file, in un solo 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 };
}Note: fraction è 0 finché il server non invia la lunghezza. Nell'app un download finisce nella cartella CACHE dell'app (non nei Download dell'utente); per dare all'utente una copia usa saveFile. Per un upload l'app prima copia il file nella sua cache, quindi i file molto grandi richiedono altrettanto spazio libero. method (predefinito 'POST') e fileName sono opzioni facoltative di upload.
appmintNative.fileViewer appmintNative#
fileViewer.open(target) from '@/lib/appmintNative'
Esempio
Apre un documento (PDF, immagine, file Office) nell'app di visualizzazione del telefono. Sul web apre l'indirizzo in una nuova scheda.
Restituisce: Promise<boolean> - true quando il visualizzatore è stato aperto, false quando è fallito (nessuna app per quel tipo, percorso errato) o la destinazione non si può aprire su questa piattaforma. Richiede: niente da attivare; usa il plugin Capacitor FileViewer incluso nell'app.
Apri un documento dal 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.');
}Apri un file scaricato prima con fileTransfer.download (un percorso locale nell'app):
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 webApri un link da un pulsante e avvisa l'utente quando nessun visualizzatore ha potuto aprirlo.
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>
);
}Note: Una destinazione http(s) usa openDocumentFromUrl; qualsiasi altra viene trattata come percorso locale (file:// viene rimosso). Sul web si aprono solo le destinazioni http(s):, blob: e data:; un percorso locale risponde false.
appmintNative.folder appmintNative#
import { folder } from '@/lib/appmintNative'
Esempio
Un'intera cartella che l'utente concede una volta e l'app mantiene tra un avvio e l'altro, con vere operazioni sui file al suo interno (nell'app: il bridge cartelle della shell).
Restituisce: ogni membro è asincrono - connected() → boolean, label() → string, request() → boolean (false se annullato), disconnect() → void, list(path) → [{ name, uri, isDirectory, length, lastModified }], readText(path) → string | null (null = file inesistente), writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean, stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. Richiede: un'app creata con l'AI attiva da sola l'interruttore Native Folder Access (SAF) quando il codice usa folder; in una build manuale spuntalo nel Passaggio 4 (Accesso).
Connetti, mostra la cartella e disconnetti:
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();
}Leggi, scrivi, elenca, rinomina ed elimina: ogni percorso è relativo alla cartella concessa ('' ne è la radice):
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 folderSalva una nota nella cartella autorizzata e dillo chiaramente quando nessuna cartella è collegata.
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');
}Note: can('folderAccess') è 'none' sul web: in un browser del telefono non esiste un equivalente di cartella persistente, quindi non mostrare lì la funzione (usa pickFile() / saveFile() per i singoli file). Chiama request() solo da un tocco dell'utente. .. e i percorsi assoluti vengono rifiutati. La cartella concessa non può mai essere rimossa. stat() funziona per file e sottocartelle (una cartella ha isDirectory: true e size: 0); stat(''), la radice, è null: usa label() per quella. request() può essere richiamata dopo un annullamento.
appmintNative.fullscreen appmintNative#
fullscreen.enter() / fullscreen.exit() / fullscreen.isOn()
Esempio
Nasconde le barre di sistema (nell'app) o entra a schermo intero nel browser (sul web), per un video, un gioco o una vista di lettura.
Restituisce: enter() e exit() restituiscono Promise<void> e non vengono mai rifiutate; isOn() restituisce boolean in modo sincrono. Richiede: niente.
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>
);
}Esci dalla modalità a schermo intero quando la vista si chiude:
import { useEffect } from 'react';
import { fullscreen } from '@/lib/appmintNative';
export function useFullscreenWhileOpen() {
useEffect(() => {
void fullscreen.enter();
return () => { void fullscreen.exit(); };
}, []);
}Nascondi le barre di sistema per una vista di lettura e segui lo stato da un pulsante.
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 };
}Note:
- Nell'app questo è lo schermo intero della shell (le barre di stato e di navigazione si nascondono); è sempre disponibile per la pagina.
- Sul web la Fullscreen API funziona solo da un tocco dell'utente. Una richiesta rifiutata viene ignorata in silenzio, quindi leggi
isOn()dopo invece di darlo per scontato.
appmintNative.geofences appmintNative#
import { geofences } from '@/lib/appmintNative'
Esempio
Geofence che scattano con l'app chiusa e sopravvivono ai riavvii: add, remove, list, events, clearEvents, onEvent, available.
Restituisce: add(spec) → Promise<BgResult> risolto quando i servizi Play hanno accettato o rifiutato ({ id, ok, reason? } - background_permission_required, location_off, too_many_geofences, …). remove / clearEvents → Promise<boolean>. list() → Promise<GeofenceSpec[]>. events() → Promise<GeofenceEvent[]> (ultimi 100, compresi quelli mentre l'app era chiusa). onEvent(cb) → un annullamento dell'iscrizione. Richiede: Geofence nella build e «Consenti sempre» (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)));Elenca i geofence attivi e le transizioni registrate mentre l'app era chiusa.
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()}`),
];
}Aggiungi una zona di lavoro per entrata e uscita, rispondi a un rifiuto in base al suo motivo e rimuovila in seguito.
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
Esempio
La forma @capacitor/geolocation: il plugin Capacitor Geolocation nell'app, lo standard navigator.geolocation sul web. Tutte restituiscono l'oggetto posizione W3C.
Restituisce: getCurrentPosition(opts) → Promise<GeolocationPosition> (viene rifiutata se negato, a timeout o senza posizione); watchPosition(opts, cb) → Promise<string> (l'id di osservazione); clearWatch({ id }) → Promise<void>; checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (ciascuno 'granted' | 'denied' | 'prompt'; su Android 12+ un utente che sceglie "Approssimativa" concede solo coarseLocation). Richiede: GPS / Posizione nel Passaggio 4 (Accesso): la build attiva entrambi quando il tuo codice chiama getCurrentPosition o watchPosition.
Dove sono, una volta sola.
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?';
}
}Traccia una corsa, poi fermati. La callback riceve null e un errore quando una rilevazione fallisce.
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; }
}Ottieni una posizione singola. Restituisce null quando l'utente rifiuta.
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
}
}Note: Funziona anche il semplice navigator.geolocation nell'app; usa questa versione quando vuoi la forma di Capacitor. Su Android 12+ l'utente può consentire solo la posizione approssimativa: ottieni comunque una posizione, solo meno precisa. Sul web requestPermissions() fa una vera richiesta di posizione per far comparire la richiesta del browser. La posizione è un dato personale: dichiaralo nel modulo Sicurezza dei dati di Play.
appmintNative.getNotificationState appmintNative#
getNotificationState(): Promise<NotificationState>
Esempio
Indica se le notifiche sono attive e, se no, se l'app può ancora chiederlo o solo le Impostazioni possono risolvere.
Restituisce: Promise<'granted' | 'prompt' | 'blocked' | 'unavailable'>. Sul web, Notification.permission mappato allo stesso modo (default → 'prompt', denied → 'blocked'); 'unavailable' dove non ci sono affatto notifiche. Richiede: niente.
import { getNotificationState } from '@/lib/appmintNative';
export async function shouldShowNotifBanner(): Promise<boolean> {
const state = await getNotificationState();
return state === 'prompt' || state === 'blocked';
}Note: ricontrolla quando l'app torna in primo piano (visibilitychange) - l'utente potrebbe aver cambiato l'impostazione.
appmintNative.getPushTags appmintNative#
import { getPushTags } from '@/lib/appmintNative'
Esempio
Legge come oggetto i tag AppMint Push di questa installazione.
Restituisce: Record<string, string> in modo sincrono - ad es. { plan: 'pro', cart: 'open' }; {} quando non ce ne sono e sul web. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
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');Imposta più tag in una sola modifica con setPushTags quando una schermata ne cambia più di uno, e svuota un valore per rimuovere il suo tag:
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)');
}Rileggi i tag per mostrare un suggerimento solo quando questa installazione ha un 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>;
}Note: questo è ciò che tiene il telefono; il centro di invio lo vede dopo il prossimo check-in dell'installazione.
appmintNative.getPushUser appmintNative#
import { getPushUser } from '@/lib/appmintNative'
Esempio
Legge l'ID utente che questa installazione ha comunicato per ultimo ad AppMint Push con setPushUser.
Restituisce: string in modo sincrono - l'id salvato, oppure '' se non hai effettuato l'accesso, non è mai stato impostato o sei sul web. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app.
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);
}Richiama setPushUser con l'id account dopo l'accesso e con '' all'uscita; restituisce false quando l'id è rifiutato o AppMint Push è disattivato:
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('');
}Leggi l'id salvato in un hook React, insieme a un tag che appartiene a quell'utente:
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;
}Note: questo è ciò che il telefono conserva; il centro di invio vede una modifica dopo il successivo check-in dell'installazione, pochi secondi dopo se è online.
appmintNative.getVolume appmintNative#
getVolume(stream) — @/lib/appmintNative
Esempio
Legge il livello del volume del telefono per un flusso: 'media', 'alarm', 'ring', 'notification' o 'call'.
Restituisce: { stream, current, max, percent, muted? } (current/max nei passi del telefono, percent 0-100), oppure null sul web. Richiede: niente.
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 risponde null sul web, quindi mostra il livello solo quando è un oggetto:
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" />;
}Segui i tasti hardware con onVolumeChange e alza un livello basso con 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
Esempio
Una vibrazione breve, dal feeling nativo, per tocchi ed esiti. Richiamala a ogni tocco significativo.
Restituisce: void (invia e dimentica). kind: 'light' (predefinito), 'medium', 'heavy', 'selection', 'success', 'warning', 'error'. Richiede: Vibrazione nel Passaggio 4 (Accesso); le build con AI la attivano per te quando il codice chiama 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>
);
}Per schede, interruttori e selettori usa 'selection'; per la conferma di un'eliminazione usa 'heavy':
import { haptic } from '@/lib/appmintNative';
function onTabChange(tab: string) { haptic('selection'); setTab(tab); }
function onConfirmDelete() { haptic('heavy'); deleteItem(); }Un pulsante "mi piace" che dà un tocco leggero all'attivazione e uno più deciso alla disattivazione:
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>
);
}Note: Nell'app usa il plugin Haptics di Capacitor (impact, notification, selectionChanged). Sul web usa navigator.vibrate dove il browser lo supporta, e altrove non fa nulla; non lancia mai errori. Per ritmi più lunghi usa hapticPlay.
appmintNative.hapticPlay appmintNative#
hapticPlay(pattern) — @/lib/appmintNative
Esempio
Riproduce uno schema di vibrazione ricco per un momento speciale (una vittoria, una serie, un battito). Usa haptic() per i tocchi normali.
Restituisce: Promise<boolean>: true quando lo schema è stato riprodotto, false quando non è stato possibile. Richiede: Vibrazione nel Passaggio 4 (Accesso).
Pattern con nome: '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();
}Il tuo schema personalizzato: un elenco di { 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
]);Abbinalo a haptic(): un normale feedback al tocco per ogni risposta, il pattern lungo solo per il momento speciale:
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
}
}Note: L'app usa il plugin Capacitor AppwrightHaptics (intensità per battito dove il telefono lo supporta). Il web ottiene solo i tempi. sharpness è accettato ma su Android non ha effetto. haptic.play(pattern) è la stessa funzione.
appmintNative.Haptics appmintNative#
Haptics — @/lib/appmintNative
Esempio
L'oggetto Haptics in stile Capacitor: gli stessi nomi e opzioni di @capacitor/haptics, che gira sullo stesso codice di haptic().
Restituisce: ogni metodo restituisce void (non una Promise; await su di esso è innocuo). Richiede: Vibrazione nel Passaggio 4 (Accesso); le build con AI la attivano per te quando il codice usa 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 300Un selettore che dà un tocco quando il valore cambia:
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>
);
}Un modulo che conferma con una vibrazione di notifica e aggiunge una breve vibrazione quando un campo è sbagliato:
import { Haptics } from '@/lib/appmintNative';
export function onSubmit(valid: boolean) {
Haptics.notification({ type: valid ? 'SUCCESS' : 'ERROR' });
if (!valid) Haptics.vibrate({ duration: 200 });
}Note: selectionStart e selectionChanged producono entrambi un tocco di selezione; selectionEnd non fa nulla. Funziona nell'app (plugin Capacitor Haptics); sul web usa navigator.vibrate dove disponibile, altrimenti non fa nulla.
appmintNative.has appmintNative#
has(cap: Capability): boolean
Esempio
La forma sì/no di can(): true quando la funzione farà qualcosa di reale su questa piattaforma.
Restituisce: boolean, in modo sincrono - esattamente can(cap) !== 'none'. Richiede: niente.
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>
);
}Proteggi un'intera funzione:
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;
}Scegli un'etichetta in base a dove gira: has() decide se mostrare il controllo, can() ti dice di che tipo è:
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';
}Note: Accetta gli stessi nomi di funzionalità di can() (vedi quella voce per l'elenco completo). Usa can() quando ti serve sapere DOVE gira ('native' o 'web'), per esempio per cambiare un'etichetta.
appmintNative.hideKeyboard appmintNative#
hideKeyboard(): void
Esempio
Chiude la tastiera a schermo, per esempio quando un modulo viene inviato o un pannello si apre sopra il campo.
Restituisce: niente. Richiede: niente.
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>
);
}Chiudi la tastiera quando un pannello si apre sopra il campo, così il pannello non resta nascosto dietro di essa:
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),
};
}Chiudila quando l'utente tocca un punto qualsiasi che non sia un campo di testo:
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>
);
}Note: Nell'app chiede al plugin Keyboard di Capacitor di nascondere la tastiera. Sul web toglie il focus al campo attivo, che è ciò che chiude la tastiera in un browser. È sicuro richiamarlo quando nessuna tastiera è aperta.
appmintNative.inAppCampaigns appmintNative#
import { inAppCampaigns } from '@/lib/appmintNative'
Esempio
I messaggi in-app che questa installazione ha adesso, come {id, name}[]: le campagne del creatore di cui questo utente fa parte del pubblico.
Restituisce: { id: string; name: string }[] in modo sincrono; [] sul web e senza AppMint Push. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
import { inAppCampaigns } from '@/lib/appmintNative';
console.log(inAppCampaigns().map((c) => c.name));Un elenco di debug in una schermata di amministrazione:
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>;
}Salta il tuo suggerimento quando il creatore ha già un messaggio con quel nome:
import { inAppCampaigns } from '@/lib/appmintNative';
export const hasCampaign = (name: string) => inAppCampaigns().some((c) => c.name === name);appmintNative.installId appmintNative#
installId(): string
Esempio
Restituisce un ID casuale stabile per questa installazione: l'ID della shell nell'app Android, un ID casuale salvato in localStorage sul web.
Restituisce: string subito. Richiede: niente.
Collega una prova gratuita al dispositivo.
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;
}Conta questa installazione una sola volta nelle tue analisi.
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');
}Mostra un breve codice di assistenza che potrai cercare in seguito, con un pulsante per copiarlo.
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>;
}Note: Resta uguale tra gli aggiornamenti dell'app e cambia con la disinstallazione o con "Cancella dati". Non è un ID hardware. Sul web genera un errore quando localStorage è bloccato, di proposito: un ID vuoto condiviso confonderebbe gli utenti.
appmintNative.isAlarmRinging appmintNative#
isAlarmRinging() — @/lib/appmintNative
Esempio
Se una delle sveglie dell'app sta suonando in questo momento.
Restituisce: boolean, in modo sincrono; sempre false sul web. Richiede: 🔔 Sveglia nel passaggio Accesso (Passaggio 4) quando crei l'app.
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;
}Mostra i pulsanti Ferma e Posticipa mentre una sveglia suona:
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>
);
}Controlla prima il supporto, così la pagina mostra i controlli della sveglia solo dove una sveglia può suonare:
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
Esempio
Ti dice se il codice gira dentro l'app Android installata (il bridge della shell window.WebToApk è presente).
Restituisce: boolean subito. false in un browser. Richiede: niente.
Mostra un badge «App Android» e un link per scaricarla nel browser.
import { isNativeApp } from '@/lib/appmintNative';
export function appBadgeText(): string {
if (isNativeApp()) return 'Android app';
return 'Get the app on Google Play';
}Etichetta gli eventi delle tue analisi con la loro provenienza.
import { isNativeApp, platform } from '@/lib/appmintNative';
export function analyticsSource(): string {
return isNativeApp() ? `app-${platform()}` : 'browser'; // 'app-android' or 'browser'
}Nascondi il banner «Scarica l'app» dentro l'app.
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>;
}Note: platform() dà la stessa risposta come stringa ('android' nell'app, 'web' in un browser). Per decidere se una funzione può essere mostrata usa invece can('<capability>'): sa quale piattaforma ha quale funzione.
appmintNative.keepAlive appmintNative#
import { keepAlive } from '@/lib/appmintNative'
Esempio
Mantiene l'app in esecuzione in background con una notifica di servizio in primo piano (start, update, stop, isRunning, onState, available).
Restituisce: start / update → Promise<BgResult> ({ ok, reason?, type?, notificationShown? }; reason: 'unsupported' in un browser o su iOS, 'not_enabled' quando la build non ha spuntato Mantieni in esecuzione in background). stop → Promise<boolean>. isRunning() → booleano. onState(cb) → una funzione per annullare l'iscrizione; cb riceve il dettaglio appmint:background-service. Richiede: Mantieni in esecuzione in background nella 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();Ferma il servizio e leggi perché start è stato rifiutato:
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.');
}
}Mantieni un pulsante React allineato al servizio e mostra l'avanzamento nella notifica:
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>
Esempio
Tiene lo schermo acceso mentre è mostrata una ricetta, un percorso, un codice QR o un timer, e poi lo lascia spegnere di nuovo.
Restituisce: Promise<boolean> - true quando la richiesta ha avuto effetto, false quando nulla ha potuto tenere acceso lo schermo (o il browser ha rifiutato). Richiede: nulla.
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 />;
}Tieni lo schermo acceso solo mentre un timer è in funzione e avvisa l'utente quando non è stato possibile.
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]);
}Controlla prima la funzionalità e riprendi il blocco quando la scheda torna in primo piano (il web lo rilascia quando la pagina è nascosta).
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);
};
}Note: Nell'app usa il plugin KeepAwake (il flag di schermo sempre acceso della finestra); sul web la Screen Wake Lock API, che i browser rilasciano da soli quando la scheda è nascosta - chiama di nuovo keepAwake(true) quando la pagina torna visibile se lo schermo deve restare acceso. Funziona solo mentre l'app è in primo piano.
appmintNative.Keyboard appmintNative#
Keyboard.hide() / Keyboard.addListener(event, handler): { remove() }
Esempio
La tastiera software nella forma del plugin Keyboard di Capacitor: chiudila e ascolta quando cambia lo spazio che copre.
Restituisce: hide() → Promise<void>. addListener(event, handler) → la forma di Capacitor: una Promise dell'handle che ha anche remove() stesso, quindi funzionano await, .then(h => h.remove()) e .remove(). event è 'keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide' o 'keyboardDidHide'; il gestore riceve { keyboardHeight }. Richiede: niente.
Chiudi la tastiera quando un modulo viene inviato o un pannello si apre sopra il campo:
import { Keyboard } from '@/lib/appmintNative';
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
await Keyboard.hide();
await save();
}Ascolta (rimuovi l'handle allo smontaggio):
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(); };
}, []);Combina entrambi per portare in vista il campo attivo quando si apre la tastiera, poi chiudi la tastiera con Invio:
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); };
}, []);
}Note: keyboardHeight è la parte di tastiera che la pagina deve ancora liberare, non l'altezza grezza: nell'app Android la shell ridimensiona la pagina per la tastiera, quindi lì vale 0 mentre la tastiera è aperta (l'evento show scatta comunque, e scatta di nuovo quando il numero cambia). Gli eventi hide scattano solo quando la tastiera si chiude davvero, quindi possono seguire «la tastiera è aperta». Gli eventi «Will» e «did» scattano insieme. Per il solo padding, onKeyboard((px) => …) è più semplice.
appmintNative.launchUrl appmintNative#
launchUrl(): Promise<string>
Esempio
Fornisce il link che ha avviato o ripreso l'app (un deep link, una scorciatoia sull'icona, un tocco sul widget, una notifica) così l'app può aprire la schermata giusta.
Restituisce: Promise<string> - l'URL completo, oppure '' quando l'app è stata aperta normalmente. Sempre '' sul web, dove la barra degli indirizzi è già la route. Richiede: niente per le scorciatoie e i tocchi sui widget; i link esterni richiedono l'opzione Deep Linking quando crei l'app.
Indirizza una sola volta quando l'app viene montata:
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]);
}Abbinalo a onDeepLink() per i link che arrivano quando l'app è già aperta:
import { onDeepLink } from '@/lib/appmintNative';
const off = onDeepLink((url) => console.log('opened with', url));Tratta la stringa vuota come «aperta normalmente» e scegli la schermata iniziale in base al link solo quando ce n'è uno. Una scorciatoia come appmint-shortcut://<package>/orders ha il percorso nel suo path.
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); });
}Note:
- Il valore viene CONSERVATO, non consumato: un componente che si monta in ritardo lo vede comunque. Un link più recente lo sostituisce.
- Viene letto dalla shell (
WebToApk.getLaunchUrl()); non richiamarlo mai direttamente in un'app creata con l'AI. Le scorciatoie e i tocchi sul widget arrivano comeappmint-shortcut://<package><route>. - Analizza con
new URL(u)e vai al suo percorso; non caricare l'URL stesso.
appmintNative.listCallLog appmintNative#
listCallLog(limit = 100, offset = 0, sinceMillis = 0): Promise<CallLogEntry[]>
Esempio
Legge il registro chiamate, dalla più recente. Solo app Android.
Restituisce: Promise<CallLogEntry[]> - ogni { number, name, type, date, duration }; type è incoming, outgoing, missed, voicemail, rejected, blocked oppure other, date è in millisecondi epoch, duration in secondi. Vuoto quando Call log è disattivato o l'autorizzazione è rifiutata. Richiede: l'interruttore Call log (read) nel Passaggio 4 (Accesso) - la build lo attiva quando il tuo codice lo richiama - e l'autorizzazione dell'utente al primo uso.
Chiamate perse degli ultimi 7 giorni.
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
}
}Tempo totale di conversazione per contatto dalle chiamate in uscita e in entrata, dal più lungo.
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]);
}Una schermata che spiega la lista vuota, perché «nessuna chiamata» e «autorizzazione rifiutata» sembrano uguali.
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 };
}Note: limit è limitato a 2000 dall'app. Un elenco vuoto non distingue «nessuna chiamata» da «autorizzazione negata». can('callLog') è 'none' sul web. Il registro chiamate è un'autorizzazione con restrizioni di Google Play: serve una Dichiarazione sulle autorizzazioni approvata in Play Console, altrimenti l'app viene rifiutata.
appmintNative.listContacts appmintNative#
listContacts(limit = 200, offset = 0): Promise<Contact[]>
Esempio
Legge la rubrica, una pagina alla volta. Solo nell'app Android, tramite il bridge della shell.
Restituisce: Promise<Contact[]>. Un array vuoto quando non ci sono contatti, quando l'accesso è negato o quando Contatti non è stato attivato in fase di build. Richiede: l'interruttore Contatti nel Passaggio 4 (Accesso) - la build lo attiva quando il tuo codice chiama questa funzione - e l'autorizzazione dell'utente al primo utilizzo.
Una schermata con la rubrica. Mostrala solo dove può funzionare. Ogni contatto è { 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
}
}Cerca nell'intera rubrica leggendola pagina per pagina in un'unica lista.
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));
}Abbinalo a sendSms per scrivere al primo numero di un contatto.
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!`);
}Note: Un risultato vuoto non distingue «nessun contatto» da «autorizzazione negata» - mostra un breve suggerimento («Consenti i contatti nelle Impostazioni») quando l'elenco è vuoto. I contatti arrivano ordinati dalla A alla Z per nome; limit è limitato a 2000. Per una singola persona usa pickContact(), che non richiede autorizzazioni. I contatti sono dati personali: dichiarali nel modulo Sicurezza dei dati di Play e nell'informativa sulla privacy.
appmintNative.listSms appmintNative#
listSms(box: 'inbox' | 'sent' | 'draft' = 'inbox', limit = 100, offset = 0): Promise<SmsMessage[]>
Esempio
Legge gli SMS dalla posta in arrivo, da quella inviata o dalle bozze, dal più recente. Solo app Android.
Restituisce: Promise<SmsMessage[]> - ciascuno { id, address, body, date, read, box }. Vuoto quando gli SMS sono disattivati, l'autorizzazione è negata o la casella è vuota. Richiede: l'interruttore SMS (lettura e invio) nel Passaggio 4 (Accesso) - la build lo attiva quando il tuo codice chiama questa funzione - e l'autorizzazione dell'utente al primo utilizzo.
Mostra gli ultimi 20 messaggi della posta in arrivo.
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
}
}Messaggi non letti di un mittente, dal più recente.
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);
}Leggi la posta inviata e le bozze in un colpo solo e ordinale insieme per data.
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));
}Note: Un elenco vuoto non permette di distinguere «nessun messaggio» da «autorizzazione rifiutata». limit è limitato a 500 dall'app. can('smsList') è 'none' sul web. I nuovi messaggi in arrivo non fanno parte di questo wrapper; le pagine scritte a mano li ricevono dall'evento appmint:sms-received (vedi listSms per il bridge grezzo). SMS è un'autorizzazione con restrizioni di Google Play: serve una Dichiarazione sulle autorizzazioni approvata in Play Console, di norma concessa solo alle app SMS predefinite.
appmintNative.liveProgress appmintNative#
import { liveProgress } from '@/lib/appmintNative'
Esempio
Una scheda di stato fissa e silenziosa nell'area notifiche di Android che aggiorni sul posto: una consegna, un caricamento, un timer, un allenamento. start, update, end.
Restituisce: start / update → Promise<boolean> (false quando non disponibile, quando le notifiche non sono consentite o quando l'utente ha disattivato la sua categoria "Stato in tempo reale"); end → Promise<void>. Richiede: l'app Android e l'autorizzazione alle notifiche su Android 13+. Sul web can('liveProgress') è 'none': mostra lì lo stesso stato dentro la tua app.
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
}Un timer in corso: startedAt mostra un orologio che conta da quel momento.
await liveProgress.start({ id: 'run', title: 'Morning run', text: '5 km goal', startedAt: Date.now() });
// ...later
await liveProgress.end('run');Gestisci un rifiuto. start risponde false; liveProgress.lastError dice perché.
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;
}Note: lo stesso id aggiorna una sola scheda; id diversi creano schede separate (l'id predefinito è 'live'). L'utente non può farla scorrere via, quindi richiama sempre end. Chiedi prima l'autorizzazione alle notifiche su Android 13+ (Notification.requestPermission()): senza di essa start risponde false e non viene mostrato nulla.
appmintNative.LocalNotifications appmintNative#
import { LocalNotifications } from '@/lib/appmintNative'
Esempio
La forma @capacitor/local-notifications (LocalNotifications.schedule({ notifications })) per il codice scritto per il plugin Capacitor. Una notifica senza schedule viene mostrata subito (tramite notify()); una con schedule: { at } è un promemoria a quell'ora, conservato anche dopo i riavvii.
Restituisce: Promise<{ notifications: { id }[] }>. Un promemoria che non si può impostare (un browser, una build senza Notifiche, autorizzazione rifiutata) viene rifiutato con il motivo: non viene mai mostrato in anticipo al suo posto. Promemoria ripetuti: scheduleAlarm o la API dei promemoria. Richiede: Android: Notifications attivato nel passaggio Accesso (Passaggio 4) quando crei l'app.
import { LocalNotifications } from '@/lib/appmintNative';
export async function announceSync(count: number) {
await LocalNotifications.schedule({
notifications: [
{ title: 'Sync complete', body: `${count} items updated`, tag: 'sync' },
],
});
}Più notifiche in una sola chiamata, ciascuna sostituisce una precedente con lo stesso 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' }] : []),
],
});
}Quando ti serve sapere se è stata mostrata, usa invece notify() e reagisci alla risposta.
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
}Note: solo immediate: i campi Capacitor id, schedule, at, actionTypeId e extra non sono supportati; ogni elemento è { title, body?, icon?, tag? }. Per una notifica in un momento successivo, una pagina scritta a mano usa WebToApk.scheduleNotificationEx (Android). Per i pulsanti usa notifyWithActions.
appmintNative.lockOrientation appmintNative#
lockOrientation(to: 'portrait' | 'landscape' | 'any'): Promise<boolean>
Esempio
Blocca lo schermo in un solo orientamento per una vista che ha senso solo così - un tavoliere di gioco, un grafico largo, un riquadro per la firma. 'any' rilascia il blocco.
Restituisce: Promise<boolean> - true quando il blocco (o il rilascio) è stato applicato, false quando la piattaforma ha rifiutato. Non viene mai rifiutata. Richiede: nulla.
Blocca mentre la vista è aperta, rilascia all'uscita:
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>;
}Controlla la risposta quando la vista ne dipende:
import { lockOrientation, toast } from '@/lib/appmintNative';
const ok = await lockOrientation('portrait');
if (!ok) toast('Turn your phone upright for the best view');Rilascia il blocco quando la schermata scompare, come hook condiviso da più schermate.
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 screenNote:
- Nell'app il plugin Capacitor ScreenOrientation applica il blocco.
- Sul web usa
screen.orientation.lock(). La maggior parte dei browser lo rifiuta fuori dallo schermo intero e i browser desktop lo rifiutano sempre, quindi lì aspettatifalse. - Dai sempre all'utente un modo per uscire da una vista bloccata.
appmintNative.longPress appmintNative#
longPress(el: HTMLElement, handler: (e: PointerEvent) => void, opts?: { ms?: number; haptics?: boolean }): () => void
Esempio
Ti chiama quando l'utente tiene premuto un dito su un elemento: per il menu contestuale di una riga, le reazioni o le azioni di una scheda.
Restituisce: una funzione per annullare l'iscrizione: chiamala allo smontaggio. Richiede: niente.
Opzioni: ms - durata della pressione, predefinita 450; haptics - vibrazione media quando scatta, predefinita true.
Apri un pannello di azioni con la pressione prolungata, con un pulsante visibile per lo stesso menu:
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>
);
}Una pressione più lenta, senza feedback tattile:
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 });Abbinalo a haptic per una conferma personalizzata e ripulisci per ogni riga della lista.
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>;
}Note:
- Scatta una volta per pressione e blocca il clic che seguirebbe, così una pressione prolungata non conta anche come «tocco».
- Uno spostamento di più di 8 px lo annulla, quindi lo scorrimento non lo attiva mai. Blocca anche il menu di pressione prolungata del browser su quell'elemento.
- Una pressione prolungata è invisibile. Mantieni sempre un pulsante visibile per la stessa azione.
appmintNative.markPushRead appmintNative#
import { markPushRead } from '@/lib/appmintNative'
Esempio
Segna come letto un messaggio della posta in arrivo di AppMint Push, oppure tutti i messaggi se chiamato senza argomento.
Restituisce: boolean in modo sincrono - true se qualcosa è cambiato. Sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
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();
}Conta i messaggi non letti dopo averne segnato uno, per un badge nella tua intestazione.
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 };
}Segna tutto come letto quando l'utente apre la schermata della posta in arrivo e aggiorna quando arriva un nuovo messaggio.
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;
}Note: lo stato di lettura vive solo su questo telefono; la notifica nell'area notifiche non viene rimossa.
appmintNative.mediaSession appmintNative#
mediaSession — @/lib/appmintNative
Esempio
Controlli di riproduzione sulla schermata di blocco e nelle notifiche per un'app che riproduce audio o video: pubblica ciò che è in riproduzione, ricevi i pulsanti Play / Pausa / Successivo / Precedente / Stop e invia un video a una TV.
Restituisce: setMetadata, setPlaybackState, clear → void. onAction(handler) → una funzione per annullare l'iscrizione. cast(url) → Promise<boolean> (false quando non si è potuto offrire nulla). Richiede: Notifica multimediale nel Passaggio 4 (Accesso) quando crei l'app; le build con AI la attivano per te quando il codice usa 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} />;
}Invia il video corrente a una 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>;
}Cancella la scheda sulla schermata di blocco quando la riproduzione finisce e lasciala stare quando la piattaforma non ha controlli multimediali.
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);
}Note:
- Su Android i pulsanti passano dal
navigator.mediaSessiondell'app, che viene installato al termine del caricamento della pagina. UnonActionregistrato prima (per esempio in un effetto di montaggio) va perso, quindi registralo alla prima riproduzione. - L'azione
seek(conpositionMs) viene consegnata solo sul web; la notifica Android non ha lo spostamento. artworkUrldeve essere un indirizzohttps://completo su internet. Su Androidcast(url)apre il selettore «Trasmetti a...» (un passaggio di consegne; si risolve contrueuna volta richiesto il selettore). Sul web usa la Remote Playback API.
appmintNative.Motion appmintNative#
Motion.addListener('accel' | 'orientation', cb): Promise<{ remove() }>
Esempio
La forma @capacitor/motion sopra gli eventi standard devicemotion e deviceorientation: movimento e inclinazione del telefono. Lo stesso codice funziona nell'app e in un browser.
Restituisce: Promise<{ remove(): Promise<void> }> - richiama remove() per fermare. 'accel' richiama con { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }; 'orientation' con { alpha, beta, gamma } (gradi). Richiede: nulla.
Scuoti per aggiornare.
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;
}Inclina una bolla di livello con l'evento di orientamento.
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();Forma hook con pulizia automatica per un componente React che legge il movimento del telefono.
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 };
}Note: Alcuni browser chiedono prima l'accesso al movimento, quindi avvialo da un tocco; se l'utente rifiuta, l'handle viene restituito ma non arriva alcun evento. can('motion') è 'none' dove il browser non ha DeviceMotionEvent. I valori mancanti dei sensori arrivano come 0. Richiama sempre remove() quando la schermata si chiude: gli eventi di movimento scattano molte volte al secondo.
appmintNative.musicVolume appmintNative#
musicVolume() — @/lib/appmintNative
Esempio
Legge il livello della musica di sottofondo in loop dell'app, da 0 a 100.
Restituisce: number, in modo sincrono: 0-100, oppure -1 quando non c'è musica (sul web, o in un'app creata senza file musicale). Richiede: un file scelto in Musica di sottofondo nel Passaggio 3 (Integra) quando crei l'app.
Mostra un cursore della musica solo quando c'è della musica:
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>
);
}Mantieni il livello in linea con un interruttore «Disattiva musica», ricordando il volume a cui tornare.
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>;
}Abbassa la musica mentre un video è in riproduzione, poi ripristinala.
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));
}Note: solo app Android; il web risponde -1. Un livello impostato con setMusicVolume viene conservato per il prossimo avvio; finché la pagina non ne imposta uno, è il livello della build.
appmintNative.Network appmintNative#
Network.getStatus(): Promise<{ connected, connectionType }> / Network.addListener(event, handler): { remove() }
Esempio
Stato online/offline nella forma del plugin Network di Capacitor - lo stesso codice di networkStatus() / onNetworkChange().
Restituisce: getStatus() → Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) → la forma di Capacitor: una Promise dell'handle che ha anche remove() stesso; il gestore riceve la stessa forma a ogni cambio. Richiede: niente.
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;
}Leggi il tipo corrente una volta e mostralo in un'etichetta di stato.
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>;
}Rimanda un download pesante quando si usano i dati mobili e gestisci anche il caso 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();
}Note: Il nome dell'evento viene ignorato: ogni listener riceve i cambi di connettività. connectionType è 'wifi', 'cellular', 'none' o 'unknown': nell'app getStatus() e il listener usano il plugin Network, sul web navigator.onLine / navigator.connection.type. connected significa che il telefono ha una rete, non che il tuo server risponde.
appmintNative.networkStatus appmintNative#
networkStatus(): { connected: boolean; type: string }
Esempio
Dice, in questo momento, se il dispositivo è online.
Restituisce: { connected, type }, in modo sincrono. type è 'wifi', 'cellular', 'none' (offline) oppure 'unknown'. Richiede: niente.
Abbastanza leggero da chiamare durante il rendering:
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>
);
}Leggi il valore iniziale, poi segui le modifiche:
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;
}Scegli una dimensione dell'immagine più leggera con i dati mobili, rispondendo anche al caso 'none' (offline).
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'}`;
}Note:
- Nell'app è la risposta del plugin Network (tenuta aggiornata in background, quindi la primissima chiamata può ancora leggere il valore della WebView). Sul web è
navigator.onLinepiùnavigator.connection.typedove il browser lo indica, altrimenti'unknown': una classe di velocità come'4g'non viene mai indicata come tipo. - "Connesso" significa che il dispositivo ha una rete, non che il tuo server risponde. Gestisci comunque le richieste fallite.
appmintNative.nfc appmintNative#
import { nfc } from '@/lib/appmintNative'
Esempio
Helper a una sola chiamata su Web NFC (NDEFReader): verifica il supporto, leggi i tag, scrivi un tag di testo. L'app Android usa il proprio NDEFReader; sul web è il Web NFC di Chrome (Chrome su Android).
Restituisce: isSupported(): Promise<boolean>; scan(onTag): Promise<() => Promise<void>> (la funzione di stop), e onTag riceve Array<{recordType, text, mediaType?}> per ogni tag: i nomi dei tipi di Web NFC ('text', 'url', 'mime', …) e il testo decodificato su ogni piattaforma; write(text): Promise<boolean> (true quando un tag è stato scritto, false in caso di errore). Richiede: su Android, attiva Supporto Web NFC nel Passaggio 4 (Accesso) quando crei l'app. scan() genera un errore quando non c'è NDEFReader: controlla prima can('nfc').
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>
);
}Scrivi un link su un tag e, se fallisce, spiega all'utente perché.
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.');
}Leggi un tag, poi smetti di scansionare.
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);
});
}Note: Su Android isSupported() è true ogni volta che la build ha l'NFC - non dice se l'NFC è attivo (lo dice window.AndroidNFC.startScan()). Dopo la funzione di arresto, onTag non viene più chiamata. Su Android l'app continua a rilevare i tag mentre è in primo piano (così un tocco non apre un'altra app), semplicemente smette di passarteli. Il text di un record di testo non ha prefisso di lingua; il text di un record URL è l'URL completo. write() attende che venga toccato un tag. Per scrivere record URL o mime e per bloccare un tag, usa direttamente NDEFReader.
appmintNative.notify appmintNative#
import { notify } from '@/lib/appmintNative'
Esempio
Mostra subito una notifica da un'app creata con l'AI, nell'app Android e sul web.
Restituisce: Promise<boolean> - true quando è pubblicata, false quando l'utente ha rifiutato l'autorizzazione alle notifiche, le notifiche sono disattivate per l'app o non sono disponibili qui. Chiede lei stessa l'autorizzazione la prima volta (Android 13+ e web). Richiede: Android: Notifications attivato nel passaggio Accesso (Passaggio 4) quando crei l'app.
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.');
}Chiedi al momento giusto: il primo notify() mostra la finestra di autorizzazione. Per chiedere da un tuo pulsante "Attiva gli avvisi", prima che venga pubblicato qualcosa, usa l'API standard, che l'app supporta anche:
async function enableAlerts() {
if (typeof Notification === 'undefined') return false;
const perm = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
return perm === 'granted';
}Apri la schermata giusta quando l'utente tocca la notifica, combinando notify con 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;
}Note: Android usa il bridge notifiche della shell (icon diventa l'icona grande; usa un'immagine https:// o data:); il web usa la Notification API. Quando l'utente ha rifiutato definitivamente, notify() restituisce false senza chiedere di nuovo: indirizzalo alle impostazioni del telefono. Per i pulsanti usa notifyWithActions; per una scheda di avanzamento fissa usa liveProgress.
appmintNative.notifyWithActions appmintNative#
import { notifyWithActions } from '@/lib/appmintNative'
Esempio
Mostra una notifica con fino a 3 pulsanti («Fatto», «Posticipa»). Il tocco torna tramite onNotificationAction.
Restituisce: Promise<boolean> - true quando è pubblicata, false quando l'autorizzazione alle notifiche è stata rifiutata (la chiede la prima volta, come notify()). Richiede: Android: Notifications attivato nel passaggio Accesso (Passaggio 4) quando crei l'app, e l'autorizzazione dell'utente su Android 13+. Sul web can('notificationActions') è '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);
});Un timer è finito: un pulsante aggiunge cinque minuti, l'altro lo ferma.
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);
});Gestisci un'autorizzazione rifiutata dopo la chiamata e fai pulizia in un componente.
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>;
}Note: Ogni pulsante mostra il suo title; il suo id è ciò che torna come e.actionId. L'opzione input (casella di risposta inline) non è supportata in questa app: viene ignorata, il pulsante non ha una casella di risposta e e.text è sempre ''. Un tocco raggiunge la pagina solo mentre l'app è in esecuzione. Sul web il wrapper pubblicherebbe una semplice notifica senza pulsanti, ed è per questo che l'esempio controlla prima can().
appmintNative.offlineBanner appmintNative#
offlineBanner(opts?: { message?: string; onlineMessage?: string }): () => void
Esempio
Aggiunge una barra pronta all'uso che scorre sotto la barra di stato quando la connessione cade e dice brevemente «Di nuovo online» quando ritorna.
Restituisce: una funzione per annullare l'iscrizione che rimuove il banner e smette di ascoltare. Richiede: nulla.
Montalo una sola volta all'avvio dell'app:
import { useEffect } from 'react';
import { offlineBanner } from '@/lib/appmintNative';
export function App() {
useEffect(() => offlineBanner(), []); // cleanup removes it
return <main>{/* … */}</main>;
}Le tue parole (per esempio nella lingua dell'app):
import { offlineBanner } from '@/lib/appmintNative';
const off = offlineBanner({ message: 'Sin conexión', onlineMessage: 'Conectado de nuevo' });Monta il banner solo per l'area dell'app con accesso effettuato e rimuovilo all'uscita dall'account.
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]);
}Note:
- Valori predefiniti: «Sei offline» e «Di nuovo online». Il messaggio online si nasconde dopo circa 1,8 secondi; quello offline resta finché la connessione non torna.
- Se il dispositivo è già offline quando viene montato, il banner compare subito.
- Costruito su
onNetworkChange(), quindi è reale su ogni superficie. I colori vengono dai token dell'app (--ink/--bgoffline,--accent/--accent-inkonline). Harole="status"per gli screen reader. - Montalo una sola volta. Due chiamate disegnano due banner.
appmintNative.onAlarm appmintNative#
onAlarm(cb) — @/lib/appmintNative
Esempio
Ascolta gli esiti della sveglia: l'app aperta da una sveglia (action: 'open', con ringing) e come è finito uno squillo: dismiss, snooze, missed (squillata fino alla fine, o sostituita da un'altra sveglia) o error. Gli esiti di quando l'app era chiusa vengono consegnati al successivo caricamento della pagina.
Restituisce: una funzione che smette di ascoltare. La funzione riceve { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. Richiede: 🔔 Sveglia nel passaggio Accesso (Passaggio 4) quando crei l'app.
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;
}Imposta una sveglia, rispondendo prima a alarmSupport().
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.';
}Ferma o posticipa la sveglia che sta suonando dalla tua schermata e reagisci all'esito.
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>
);
}Note: anche i normali promemoria che aprono l'app arrivano qui con action: 'open' e ringing: false.
appmintNative.onBackGesture appmintNative#
onBackGesture(handler: () => boolean): () => void
Esempio
Permette a un foglio, una modale, un lightbox o un passaggio guidato aperto di rispondere per primo al gesto INDIETRO: lo swipe dal bordo o il tasto indietro di Android nell'app, il tasto indietro del browser sul web.
Restituisce: una funzione per annullare l'iscrizione: richiamala quando la vista si chiude o viene smontata. Il tuo gestore restituisce true per consumare la pressione (hai chiuso qualcosa) oppure false per lasciarla passare. Richiede: nulla.
Registrati solo mentre il pannello è aperto:
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>}
</>
);
}Torna indietro passo dopo passo in una procedura guidata e lascia che l'indietro dell'ultimo passaggio esca normalmente:
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]);
}Chiudi una lightbox con il gesto indietro, tenendo la registrazione legata all'apertura dell'immagine.
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;
}Note:
- I gestori formano una pila: l'ultimo registrato (il pannello in alto) viene interrogato per primo. Un gestore che genera un errore non consuma nulla.
- Su Android passa dal gestore del tasto indietro della shell. Senza di esso, il gesto indietro con un foglio aperto chiude l'intera app.
- Sul web aggiunge una voce alla cronologia finché c'è un gestore registrato e la rimuove quando l'ultimo annulla l'iscrizione.
- Mostra comunque un controllo indietro / chiudi visibile: molti utenti non usano mai il gesto indietro.
- React Router gestisce già il normale ritorno alla pagina precedente; usalo solo per lo stato che il router non conosce. Annulla sempre l'iscrizione, altrimenti un pannello chiuso continua a intercettare i tocchi su indietro.
appmintNative.onDeepLink appmintNative#
onDeepLink(cb: (url: string) => void): () => void
Esempio
Ti chiama quando un link apre l'app mentre è già in esecuzione - un deep link, una scorciatoia dell'icona, un tocco su un widget.
Restituisce: una funzione per annullare l'iscrizione: richiamala allo smontaggio. Richiede: nulla per le scorciatoie e i tocchi sul widget; i link esterni richiedono l'opzione Deep Linking quando crei l'app.
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { launchUrl, onDeepLink } from '@/lib/appmintNative';
export function DeepLinkRouter() {
const navigate = useNavigate();
useEffect(() => {
const go = (u: string) => { const { pathname, search } = new URL(u); navigate(pathname + search); };
void launchUrl().then((u) => { if (u) go(u); }); // the link that started the app
return onDeepLink(go); // links while it runs; returns the unsubscribe
}, [navigate]);
return null;
}Una pagina scritta a mano (senza React) può ascoltare direttamente lo stesso evento della shell:
window.addEventListener('appmint:deep-link', function (e) {
var url = e.detail && e.detail.url;
if (url) console.log('deep link', url);
});Apri un prodotto specifico quando si tocca un widget o una scorciatoia, ignorando i link che non sono nostri.
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { onDeepLink } from '@/lib/appmintNative';
export function ProductLinks() {
const navigate = useNavigate();
useEffect(() => onDeepLink((url) => {
const m = new URL(url).pathname.match(/^\/product\/(\w+)$/);
if (m) navigate(`/products/${m[1]}`);
}), [navigate]);
return null;
}Note:
- Android: ascolta l'evento
appmint:deep-linkdella shell (detail.url); la shell richiama anchewindow.onAppMintDeepLink(detail)quando una pagina la definisce. Dopo l'arrivo di un link, lo restituisce anchelaunchUrl(). - Web: non scatta mai (la callback non viene chiamata) e l'annullamento dell'iscrizione non fa nulla.
appmintNative.onInAppMessage appmintNative#
import { onInAppMessage } from '@/lib/appmintNative'
Esempio
Ti chiama per ogni messaggio in-app (le campagne del creatore) mostrato, cliccato, chiuso o con risposta su questa installazione. Restituisce la funzione per annullare l'iscrizione.
Restituisce: () => void. La callback riceve {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. Non scatta mai sul web. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app.
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]);
}Disegna tu i messaggi (con setInAppPresentation('none'), presented è false e message contiene il design):
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) };
}Reagisci a un pulsante toccato dall'utente nel disegno dell'app:
import { onInAppMessage } from '@/lib/appmintNative';
onInAppMessage((e) => {
if (e.type === 'clicked' && e.link === 'appmint://notifications/enable') refreshNotificationBadge();
});Note: button è -1 per un tocco sul messaggio stesso, altrimenti l'indice del pulsante.
Risposte (type: 'answered'). Un messaggio con una fila di stelle (sondaggio a stelle, Valutaci) o chip di punteggio 0-10 (punteggio NPS) riporta la risposta una sola volta, alla chiusura: question è 'stars' (valore 1-5) o 'nps' (valore 0-10), e template indica il modello della galleria da cui proviene. Il centro di invio mostra la media o il punteggio NPS; il tuo codice può dare un seguito:
import { onInAppMessage } from '@/lib/appmintNative';
onInAppMessage((e) => {
if (e.type === 'answered' && e.question === 'nps' && (e.value ?? 10) <= 6) openFeedbackSheet();
});Tieni un tuo registro delle valutazioni a stelle:
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 }) });
});Ringrazia chi ha dato una valutazione alta, in un componente 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
Esempio
Ti dice quanti pixel della pagina copre la tastiera sullo schermo, ogni volta che cambia, così una barra in basso può stare sopra di essa.
Restituisce: una funzione per annullare l'iscrizione - chiamala allo smontaggio. La funzione riceve un numero in pixel CSS: il margine inferiore extra che ti serve, 0 quando la tastiera è chiusa. Richiede: niente.
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>
);
}Tieni una lista di chat scorsa fino al messaggio più recente mentre la tastiera si apre:
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>;
}Un hook riutilizzabile, con uno spazio di area sicura che scompare mentre la tastiera è aperta:
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>Note:
- Nell'app ascolta il plugin Keyboard di Capacitor e riporta solo la parte di tastiera che il layout non ha già assorbito. La shell di solito ridimensiona la pagina per la tastiera, quindi il valore lì è spesso
0: è corretto; non aggiungere da solo l'altezza grezza della tastiera. - In un'app a schermo intero o in modalità kiosk (pagina non ridimensionata) restituisce l'altezza completa della tastiera.
- Sul web usa
visualViewport, richiama la callback una volta subito e tratta meno di 80 px come interfaccia del browser (riportato come0). - Nell'app la prima chiamata arriva quando la tastiera si apre per la prima volta, quindi inizia il tuo stato a
0.
appmintNative.onNetworkChange appmintNative#
onNetworkChange(cb: (status: { connected: boolean; type: string }) => void): () => void
Esempio
Ti richiama ogni volta che la connessione cade o ritorna: per un banner offline o per ritentare le scritture in coda.
Restituisce: una funzione per annullare l'iscrizione: chiamala allo smontaggio. Richiede: niente.
Riprova il lavoro in coda quando il dispositivo torna 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]);
}Mostra subito lo stato attuale, poi segui le modifiche (la callback non viene chiamata all'iscrizione):
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;
}Rimanda i download pesanti quando usi i dati mobili e tratta 'none' come 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]);
}Note:
- NON ti richiama con lo stato attuale quando ti iscrivi. Leggi
networkStatus()per il valore iniziale. - Nell'app ascolta il plugin Capacitor Network (
networkStatusChange);typeè'wifi','cellular','none'o'unknown'. - Sul web ascolta gli eventi
online/offlinee riferiscenetworkStatus(). - Per un banner già pronto, usa
offlineBanner(), che si basa su questo.
appmintNative.onNotificationAction appmintNative#
import { onNotificationAction } from '@/lib/appmintNative'
Esempio
Ascolta i tocchi sui pulsanti delle notifiche (da notifyWithActions).
Restituisce: una funzione che smette di ascoltare. La funzione riceve { actionId, tag } - l'ID del pulsante e il tag della notifica. L'app Android non invia alcun text di risposta (la risposta in linea non è supportata), quindi non fare affidamento su e.text. Richiede: lo stesso di 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]);
}Evento grezzo in una pagina scritta a mano (app Android):
window.addEventListener('appmint:notification-action', function (e) {
// e.detail = { actionId: 'Mute', tag: 'chat-7' }
console.log(e.detail.actionId, e.detail.tag);
});Pubblica una notifica con pulsanti solo dove la build li supporta e gestisci il tocco tramite il tag della notifica:
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]);
}Note: nell'app Android questo è l'evento appmint:notification-action (anche window.onAppMintNotificationAction(detail)); sul web non fa nulla. Un tocco viene consegnato solo mentre l'app è in esecuzione: toccare un pulsante non apre l'app.
appmintNative.onNotificationClick appmintNative#
import { onNotificationClick } from '@/lib/appmintNative'
Esempio
Esegue una funzione quando l'utente tocca una notifica pubblicata dall'app - compreso il tocco che avvia l'app da zero - così l'app può aprire la schermata giusta.
Restituisce: la funzione per annullare l'iscrizione. La callback riceve { tag, title, body, data } - data è ciò che hai passato a notify({ …, data }). Richiede: notifiche, come notify(). Evento sottostante: 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 } });
}Apri la schermata giusta dopo il tocco, anche quando ha avviato l'app da zero, e ignora le notifiche che non portano dati:
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]);
}Invia una notifica solo dove le notifiche esistono, con dei dati per il tocco (data richiede un 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');
}Note: Il tocco attraversa la splash e la schermata del PIN fino alla pagina. data deve essere JSON (sotto i 16 KB) e richiede un tag.
appmintNative.onPullToRefresh appmintNative#
onPullToRefresh(el: HTMLElement, onRefresh: () => void | Promise<void>, opts?: { threshold?: number }): () => void
Esempio
Aggiunge il gesto di trascinamento verso il basso per aggiornare a una lista scorrevole: trascina in alto, rilascia e i dati si ricaricano.
Restituisce: una funzione per annullare l'iscrizione: chiamala allo smontaggio. Richiede: niente.
el è l'elemento che scorre (oppure document.documentElement quando scorre l'intera pagina). Lo spinner resta finché la promise di onRefresh non si conclude:
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>;
}Una pagina che scorre per intero, con un trascinamento più lungo:
import { onPullToRefresh } from '@/lib/appmintNative';
declare function reloadFeed(): Promise<void>;
const off = onPullToRefresh(document.documentElement, reloadFeed, { threshold: 96 });Aggiorna una lista tramite un hook dei dati, con un tocco aptico quando i dati sono arrivati:
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">
}Note:
- Parte solo quando la lista è scorsa fino in cima. Il valore predefinito di
thresholdè 72 px di trascinamento. - Disegna un indicatore rotondo con i colori dell'app (
--surface,--accent), produce un feedback tattile medio al punto di conferma e ignora nuovi trascinamenti mentre un aggiornamento è in corso. - Aggiorna i tuoi DATI, non la pagina. Il ricaricamento della pagina con trascinamento della shell (Attiva Trascina per aggiornare nel Passaggio 3 (Integra)) è disattivato per impostazione predefinita: lascialo disattivato quando usi questo, così un trascinamento non ricarica anche la pagina.
- Basato sui Pointer Events, quindi è uguale su ogni piattaforma. Imposta
overscroll-behavior-y: containsuele lo ripristina quando annulli l'iscrizione.
appmintNative.onVolumeChange appmintNative#
onVolumeChange(cb) — @/lib/appmintNative
Esempio
Ti richiama con cb ogni volta che cambia uno dei livelli del volume del telefono: i tasti fisici, il cursore di sistema, un'altra app.
Restituisce: una funzione che smette di ascoltare (l'ultima a fermarsi spegne il controllo nativo). La callback riceve { stream, current, max, percent, muted? } per ogni canale cambiato. Sul web non fa nulla. Richiede: nulla.
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>;
}Avvisa quando il volume multimediale è al minimo mentre un video è in riproduzione e smetti di ascoltare allo smontaggio:
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]);
}Rispecchia il livello del telefono su un cursore e riscrivi la modifica dell'utente (il web non ha volume, quindi getVolume lì è null):
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>
Esempio
Apre la schermata delle impostazioni delle notifiche di questa app (app Android).
Restituisce: Promise<boolean> - true quando la schermata si è aperta; false sul web e ovunque non esista tale schermata. Controlla can('notificationSettings') prima di mostrare il controllo. Richiede: nulla.
import { can, openNotificationSettings } from '@/lib/appmintNative';
export function NotificationSettingsRow(row: HTMLElement) {
row.hidden = can('notificationSettings') === 'none';
row.addEventListener('click', () => { void openNotificationSettings(); });
}Note: per un «Attiva le notifiche» con un solo tocco usa enableNotifications(), che apre questa schermata solo quando la finestra dell'autorizzazione non può più essere mostrata.
appmintNative.passkeys appmintNative#
passkeys — available / create / get / origin / assetLinks
Esempio
Accesso e registrazione con passkey usando le opzioni JSON create dal tuo server di passkey. Nell'app Android usa il gestore di passkey di Android; sul web usa navigator.credentials.
Restituisce: available() → 'native' | 'web' | 'none'; create(optionsJSON) → Promise del RegistrationResponseJSON; get(optionsJSON) → Promise del AuthenticationResponseJSON (invia l'uno o l'altro al tuo server con POST). Vengono rifiutate con una DOMException (NotAllowedError, InvalidStateError, SecurityError, NotSupportedError). origin() → stringa, assetLinks() → array (solo nell'app; '' / [] sul web). Richiede: un server per le passkey; nell'app anche Android 9+ con i servizi Google Play e il file del tuo dominio 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),
});
}Valori di configurazione una tantum, dall'app installata tramite 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 originsOffri il pulsante passkey solo dove può funzionare e dillo quando non può:
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>;
}Note: Sul web serve una pagina sicura (https://). Una pagina inclusa nell'app non ha un dominio: le opzioni del tuo server devono impostare rp.id / rpId sul tuo dominio. La compilazione automatica delle passkey (mediation: 'conditional') non è disponibile nell'app - parti da un pulsante.
appmintNative.paste appmintNative#
paste(): Promise<string>
Esempio
Legge il testo negli appunti di sistema.
Restituisce: Promise<string> - il testo negli appunti, oppure '' quando gli appunti sono vuoti. VIENE RIFIUTATA quando questo browser non può leggere affatto gli appunti, o quando l'utente nega l'autorizzazione. Richiede: niente.
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>
);
}La forma in stile Capacitor:
import { Clipboard } from '@/lib/appmintNative';
const { value } = await Clipboard.read();Incolla in una casella di testo con un messaggio per ogni esito, e abbinalo a 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');
}Note: Nell'app il plugin Clipboard legge il testo; degli appunti vuoti restituiscono '', non un errore. In un browser è navigator.clipboard.readText() (solo Chromium), che chiede l'autorizzazione all'utente: richiamalo da un tocco.
appmintNative.pickContact appmintNative#
pickContact(): Promise<Contact | null>
Esempio
Apre il selettore contatti di sistema così l'utente sceglie una persona: il selettore di Android nell'app, la Contact Picker API in Chrome su Android.
Restituisce: Promise<Contact | null> - null quando l'utente annulla o l'accesso è negato. Richiede: niente (il selettore di sistema non richiede autorizzazioni).
Scegli una persona e compila un modulo. Il contatto ha la stessa forma nell'app e sul 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
}
}Un pulsante "Scegli un contatto" che mostra la scelta e una semplice casella per il numero dove il selettore non esiste:
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>
);
}Passa il numero scelto all'app di messaggi, con un avviso quando il contatto non ne ha:
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)}`;
}Note: Nell'app torna solo il numero toccato, mai le email, e photoUri è un indirizzo content:// della foto del contatto. can('contactPick') è 'none' nei browser senza la Contact Picker API (desktop, Safari): non mostrare lì il pulsante.
appmintNative.pickFile appmintNative#
pickFile(accept?) from '@/lib/appmintNative'
Esempio
Permette all'utente di scegliere un file e leggerlo come testo. Nell'app apre il selettore di documenti di Android; sul web apre un normale campo file.
Restituisce: Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }, oppure null solo quando l'utente annulla. Viene rifiutata con un Error il cui code è 'no-picker' (il telefono non ha un'app di selezione documenti), 'disabled' oppure 'failed' quando nessun selettore ha potuto aprirsi. text() legge l'intero file e viene rifiutata con il codice del bridge (per esempio 'not-found') quando non può. Richiede: niente. Con Accesso nativo alle cartelle (SAF) spuntato nel Passaggio 4 (Accesso) l'app usa il selettore di documenti di Android; senza, l'app usa il selettore di file della pagina.
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)}`);
}
}Importa un backup JSON e gestisci ogni modo in cui pickFile può fallire:
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');
}
}Scegli un file di testo e mettilo in un editor, accanto ai dettagli del 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>
);
}Note: Nell'app text() legge il file a blocchi di 4 MB fino alla fine e lo decodifica come UTF-8, quindi i file grandi vengono letti per intero: mantieni un tuo limite di dimensione adatto a ciò che la schermata può gestire. Un selettore che non riesce ad aprirsi viene rifiutato, quindi avvolgi anche pickFile() in try/catch. Sul web text() è il File.text() del browser. Per file binari o per riscrivere, le pagine scritte a mano usano WebToApkFS.
appmintNative.pip appmintNative#
pip.enter(video?) / pip.exit(video?)
Esempio
Mantiene un video in riproduzione in una piccola finestra mobile mentre l'utente fa altro.
Restituisce: enter() restituisce Promise<boolean> - true quando la finestra flottante si è aperta. exit() restituisce Promise<void>. Nessuna delle due viene rifiutata. Richiede: nell'app Android l'opzione Picture-in-Picture (PiP) nel Passaggio 3 (Integrazione) della build - si attiva da sola quando il tuo codice chiama pip.enter(.
Un pulsante "riduci a icona" su un player:
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>
);
}Esci quando il lettore si chiude (web):
import { pip } from '@/lib/appmintNative';
declare const video: HTMLVideoElement;
await pip.exit(video);Entra automaticamente in picture-in-picture quando l'utente lascia un video in riproduzione, solo dove è supportato:
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]);
}Note:
- Android (8+): TUTTA l'app si riduce nella finestra flottante e l'argomento
videoviene ignorato. Restituiscefalsesu Android più vecchi o in una build senza l'opzione PiP. L'utente espande la finestra per tornare;exit()lì non fa nulla. - Il web: il picture-in-picture dell'elemento
<video>. Il video deve essere caricato (di solito in riproduzione); una chiamata senzavideorestituiscefalse. - Richiamalo da un tocco.
appmintNative.platform appmintNative#
platform(): 'android' | 'ios' | 'web'
Esempio
Ti dice su quale piattaforma gira il codice: l'app Android installata o un browser web.
Restituisce: una stringa, subito: 'android' nell'app, 'web' in un browser. Chiede prima a Capacitor (Capacitor.getPlatform()), poi controlla il bridge della shell Android, altrimenti 'web'. Il tipo elenca anche 'ios', ma un'app AppMint non lo riceve mai: AppMint crea solo app Android. Richiede: niente.
Scegli il link giusto e copialo.
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';Mostra controlli solo per l'app.
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>;
}Etichetta gli eventi delle analisi con la piattaforma.
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 }),
});
}Note: platform() non risponde mai 'web' dentro l'app installata. Non usarlo per decidere se una funzione esiste - molte funzioni dipendono anche dagli interruttori della build; can('<capability>') risponde a questo.
appmintNative.privacyScreen appmintNative#
privacyScreen — enable() / disable() / isEnabled()
Esempio
Blocca screenshot, registrazione dello schermo e anteprima nel selettore delle app mentre è aperta una schermata con dati riservati.
Restituisce: enable() → Promise<boolean> (false dove nulla può bloccarlo); disable() → Promise<void>; isEnabled() → Promise<boolean>. Richiede: niente. Nell'app Android usa il blocco screenshot della shell (screenshot, registrazione dello schermo, trasmissione e miniatura nelle app recenti).
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
}, []);
}Di' all'utente cosa è protetto, ma solo quando lo è davvero:
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' : '';
}Attiva la protezione per una vista sensibile e avvisa l'utente se non è stato possibile applicarla:
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();
}Note: Nell'app il blocco appartiene alla pagina corrente e viene rilasciato quando l'app passa a un'altra route, quindi riattivalo quando quella schermata viene mostrata di nuovo. Sul web can('privacyScreen') è 'none' e enable() restituisce false: non dichiarare protezione lì. Nulla impedisce a una fotocamera puntata sullo schermo.
appmintNative.pushInbox appmintNative#
import { pushInbox } from '@/lib/appmintNative'
Esempio
Restituisce i messaggi AppMint Push ricevuti da questa installazione (dal più recente, fino a 50), per una schermata di notifiche in-app.
Restituisce: PushMessage[] in modo sincrono: { id, title, body, imageUrl, link, receivedAt, read }. Un array vuoto sul web e quando AppMint Push è disattivato. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
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 };
}Mostra un conteggio di non letti che segue i nuovi arrivi:
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;
}Mostra la lista, con uno stato vuoto per il web e per le build senza AppMint Push, e un pulsante «Segna tutto come letto»:
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>
);
}Note: la posta in arrivo è la verità salvata: leggila al montaggio; l'evento appmint:push scatta solo mentre l'app è sullo schermo. I messaggi chiusi vengono esclusi.
appmintNative.readBarcodeFrom appmintNative#
readBarcodeFrom(source: Blob | CanvasImageSource, formats?: BarcodeFormat[]): Promise<{ value, format }[]>
Esempio
Legge codici a barre e codici QR da un'immagine che la pagina ha già: un file foto, un Blob, un <img>, un <canvas> o un fotogramma video. Non viene aperta la fotocamera.
Restituisce: Promise<{ value: string; format: string }[]> - ogni codice trovato, nell'ordine del rilevatore; un array vuoto quando non ce ne sono, quando l'immagine non si può leggere o quando la lettura dei codici a barre non è disponibile. Non viene mai rifiutata. Richiede: nulla.
Leggi un codice da una foto scelta dall'utente:
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>
);
}Da un'immagine già presente nella pagina:
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);Leggi un codice da un fotogramma canvas, controllando prima cosa supporta il dispositivo:
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;
}Note:
- Usa il
BarcodeDetectordel motore del browser (presente nella WebView dell'app Android; assente in Safari e in molti browser desktop: in quel caso restituisce[]). - Un semplice
<input type="file" accept="image/*">apre il selettore foto di Android e non richiede l'autorizzazione all'archiviazione. - Restringi
formatsquando li conosci: è più veloce e legge meno male.barcodeFormats()elenca ciò che il dispositivo supporta.
appmintNative.readOtp appmintNative#
import { readOtp } from '@/lib/appmintNative'
Esempio
Attende il codice monouso via SMS per questa app e lo restituisce: il pannello di consenso SMS di Google nell'app Android, WebOTP in Chrome sul web.
Restituisce: Promise<string | null> - il codice, oppure null quando l'utente ha rifiutato, sono passati cinque minuti o signal ha interrotto. Genera un errore quando can('otp') è 'none'. Richiede: niente da attivare. L'SMS deve terminare con @<host> #<code> (nell'app Android l'host è 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" />
);
}Gestisci la risposta «nessun codice» e offri un nuovo invio quando l'attesa finisce senza codice:
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>
);
}Intercetta l'eccezione quando non puoi controllare prima can('otp'):
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
}
}Note: è un acceleratore: lascia sempre il campo digitabile.
appmintNative.recognizeText appmintNative#
import { recognizeText } from '@/lib/appmintNative'
Esempio
Legge il testo in un'immagine sul dispositivo (can('ocr')): il riconoscitore ML Kit dell'app Android, oppure il TextDetector di Chrome dove è attivo.
Restituisce: Promise di { text, blocks: [{ text, box, lines: [{ text, box }] }] }, riquadri in pixel dell'immagine. Viene rifiutata con code 'unavailable' dove can('ocr') è 'none', oppure con una DOMException quando il riconoscimento fallisce. Richiede: niente, nessuna autorizzazione.
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>
);
}Gestisci in modo esplicito i casi "nessun riconoscitore" e di errore:
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
}
}Leggi una scrittura diversa dal latino ed elenca ogni riga con la sua posizione:
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 }));
}Note: Solo alfabeti latini. La prima chiamata su un telefono appena configurato può richiedere qualche secondo mentre i servizi Play scaricano il modello - mostra l'avanzamento.
appmintNative.remoteConfig appmintNative#
remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()
Esempio
Mostra la striscia di annuncio e la schermata di manutenzione che il creatore pubblica da Remote Update, per la pagina quando gira in un browser web. Nell'app Android la shell lo fa già da sola, quindi qui queste chiamate non fanno nulla.
Restituisce: start(manifestUrl?) → Promise<void> (fetch + disegno; non viene mai rifiutata); fetch(url) → Promise<RemoteConfig | null> (non viene mai rifiutata; null = non si applica nulla); apply(cfg) → una funzione di smontaggio; manifestUrl() → string (il content di <meta name="appmint-remote-manifest">, '' se non ce n'è); handledHere() → boolean; dismissed(id) → boolean; dismiss(id) → void. Richiede: niente nell'app: lì Remote Update è nativo.
Sicuro da chiamare all'avvio; nell'app restituisce subito:
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>;
}Oppure leggi i valori e disegna la tua interfaccia (solo 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)Mostra tu stesso il testo di manutenzione sul web e lascia che l'app usi la propria schermata:
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;
}Note:
RemoteConfig={ version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }.handledHere()èfalsenell'app Android: la shell legge il manifest in modo nativo e disegna lei stessa l'annuncio, così non vedi mai due banner.start()senza argomento usamanifestUrl(). Una build AppMint non aggiunge quel meta tag, quindi senza argomento (o senza il tuo<meta name="appmint-remote-manifest" content="…">) non viene scaricato nulla.fetch()restituiscenullper una pubblicazione sospesa o annullata, un manifest il cui elencoplatformsnon includeweb, un formato di manifest più recente o un errore di rete.- La modalità di manutenzione ha la precedenza:
apply()allora copre l'intera pagina con il testo di manutenzione. Un annuncio chiuso resta nascosto finché non viene pubblicato un nuovoannouncementId.
appmintNative.requestDndAccess appmintNative#
requestDndAccess() — @/lib/appmintNative
Esempio
Chiede l'accesso a Non disturbare, che Android richiede prima che l'app possa cambiare il volume della suoneria o delle notifiche mentre Non disturbare è attivo.
Restituisce: 'granted', 'opened' (è stata mostrata la schermata delle impostazioni: ricontrolla quando l'utente torna), 'not_declared', oppure 'unavailable' sul web. Richiede: niente da spuntare: chiamarla nel tuo codice fa sì che la build dichiari l'autorizzazione.
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';
}Chiedi una volta, poi riprova la modifica del volume dopo che l'utente torna dalle impostazioni:
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;
};
}Gestisci ogni risposta che può dare:
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>
Esempio
Chiede a Google Play di mostrare la sua finestra di valutazione dentro l'app (Play In-App Review).
Restituisce: Promise<boolean> - true solo quando Play ha mostrato il pannello; false sul web, per un'installazione che non viene da Google Play, quando Play ha rifiutato (la sua quota) o è fallito. Usa askForReview() per capire quale caso. Richiede: niente; il pannello compare solo per un'app installata da 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();
}
}Scopri perché il pannello non è comparso, senza mandare via l'utente:
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);
}Chiedi solo dopo un momento di successo, e al massimo una volta al mese:
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();
}Note: Google Play limita la frequenza del pannello e spesso non mostra nulla: non premiare mai una recensione e non creare mai un pulsante "Valutaci" che si aspetta l'apertura del pannello (usa askForReview({ openStore: true }) per un pulsante). Non esce mai dall'app per andare alla scheda su Play.
appmintNative.restorePushMessage appmintNative#
import { restorePushMessage } from '@/lib/appmintNative'
Esempio
Annulla dismissPushMessage(id): il messaggio torna nella posta in arrivo di AppMint Push al suo posto originale.
Restituisce: boolean in modo sincrono - true se è stato ripristinato; false se l'id è sconosciuto o non era stato eliminato (sempre false sul web). Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
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();
}Offri un Annulla subito dopo uno scorrimento per eliminare, in una piccola riga React:
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>
);
}Controlla la risposta, perché un ID sconosciuto o mai chiuso dà 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)`;
}Note: i messaggi rimossi con clearPushInbox() o ritirati da te non si possono ripristinare.
appmintNative.safeArea appmintNative#
safeArea(): { top: number; right: number; bottom: number; left: number }
Esempio
Restituisce lo spazio occupato dalla barra di stato, dall'area della fotocamera e dalla barra di navigazione, in pixel CSS, così le barre fisse non finiscono sotto di esse.
Restituisce: numeri { top, right, bottom, left }, in modo sincrono. Richiede: niente.
Il più delle volte basta il CSS: il runtime inserisce --safe-top, --safe-right, --safe-bottom, --safe-left su :root:
export function BottomNav() {
return (
<nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
{/* tabs */}
</nav>
);
}Usa i numeri quando il codice deve fare i calcoli, per esempio per posizionare un pulsante flottante:
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;
}Tieni una lista scorrevole libera dalle barre dimensionando il suo padding in base ai numeri:
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>;
}Note:
- Misurato dal CSS
env(safe-area-inset-*). Il valore viene messo in cache e rimisurato dopo un ridimensionamento o una rotazione della finestra. - Prima che
document.bodyesista (molto presto durante l'import) restituisce tutti zeri: chiamala da un effect. - In un browser desktop tutti e quattro sono di solito
0.
appmintNative.saveFile appmintNative#
saveFile(fileName, data, mime?) from '@/lib/appmintNative'
Esempio
Salva testo o un Blob come file che l'utente conserva, con il nome che scegli. Nell'app apre il foglio «Salva con nome…» di Android con quel nome; in un browser è un normale download.
Restituisce: (sincrono) boolean - true quando il download è partito, false quando non c'è document o la sua creazione ha generato un errore. Non dice se l'utente ha salvato o annullato il pannello. Richiede: niente.
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.');
}Un Blob mantiene il proprio tipo (il terzo argomento viene ignorato):
import { saveFile } from '@/lib/appmintNative';
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
saveFile('Backup.json', blob);Salva il risultato di un fetch e avvisa quando non è potuto partire:
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');
}Note: Sotto il cofano è un semplice <a download>, che l'app trasforma nel pannello "Salva come…" (vedi downloads). Passa sempre un nome con estensione; senza, il file viene salvato come download. Il tipo MIME predefinito per una stringa è text/plain.
appmintNative.saveToGallery appmintNative#
import { saveToGallery } from '@/lib/appmintNative'
Esempio
Salva una foto o un video nella galleria del telefono: can('gallery') è 'native' nell'app Android e 'web' in un browser (lì il file viene scaricato).
Restituisce: Promise dell'URI dell'elemento salvato ('' sul web). Viene rifiutata con code 'permission-denied', 'download-failed', 'bad-source' o 'failed'. Richiede: niente; nessuna autorizzazione su 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>
);
}Salva un videoclip e gestisci i codici di errore con cui può essere rifiutato:
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');
}
}Salva un'immagine da un data URL e passala al pannello di condivisione solo se è stata salvata:
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' });
}Note: source può essere anche un URL data: o un URL http(s); passa { kind: 'video' } per l'URL di un video senza tipo MIME video.
appmintNative.scanBarcode appmintNative#
scanBarcode(video: HTMLVideoElement, opts?: { signal?, formats?, facingMode? }): Promise<{ value, format } | null>
Esempio
Apre la fotocamera in un <video> che TU posizioni nel tuo layout e si risolve con il primo codice a barre o QR che vede.
Restituisce: Promise<{ value: string; format: string } | null> - il primo codice, oppure null quando annullato (il signal è stato interrotto), quando la fotocamera è stata rifiutata o quando la scansione non è disponibile. Non viene mai rifiutata e ferma sempre la fotocamera. Richiede: l'accesso alla fotocamera: nell'app Android attiva Camera nel Passaggio 4 (Accesso) quando crei l'app.
Usalo solo quando il mirino deve stare dentro la tua schermata; altrimenti scanCode() è più semplice.
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>
);
}Usa la fotocamera frontale:
import { scanBarcode } from '@/lib/appmintNative';
declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });Chiudi lo scanner dopo la prima lettura e offri un codice da digitare quando la fotocamera non è disponibile:
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>
);
}Note:
- Chiama
getUserMedia(così l'utente vede la richiesta di autorizzazione alla fotocamera) e decodifica conBarcodeDetectora ogni fotogramma.can('barcodeScan')è'none'dove manca uno dei due. - La build analizza il tuo codice: una chiamata
scanBarcode(attiva per te Fotocamera. Se l'hai disattivata a mano, la richiesta alla fotocamera fallisce e ottieninull. - Avvialo da un'azione dell'utente (un pulsante Scansiona o l'apertura di una schermata di scansione), mai all'avvio dell'app. Gestisci
nullcome un esito normale con un'opzione di inserimento manuale.
appmintNative.scanCode appmintNative#
scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>
Esempio
Apre uno scanner a schermo intero e si risolve con il primo codice a barre o QR - una sola chiamata, niente da disegnare.
Restituisce: Promise<ScanCodeResult | null> - { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }, oppure null quando l'utente ha chiuso lo scanner. Qualsiasi altra cosa viene rifiutata con un Error il cui code è 'unavailable' (qui non c'è nessuno scanner: Play services mancanti nell'app; né BarcodeDetector né fotocamera sul web), 'busy' (una scansione è già aperta), 'camera-denied' (web: autorizzazione alla fotocamera rifiutata) o 'failed'. Richiede: niente nell'app (lo scanner di Google non richiede l'autorizzazione alla fotocamera).
Da un pulsante Scansiona:
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>
);
}Usa le parti già interpretate che l'app compila (Wi-Fi, link, contatti …), con un'alternativa da digitare nello scanner di 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);Posticipa da un pulsante nella tua schermata della sveglia, usando onAlarm per sapere quando sta suonando.
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 risponde false quando non suona nulla, quindi controlla prima di promettere all'utente un secondo squillo.
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.';
}Note:
- Nell'app è lo scanner di codici di Google (servizi Play, tramite
AppMint.scanCodedella shell): zoom automatico, nessuna richiesta per la fotocamera.typeè'url','wifi','text','phone','email','sms','geo','contact','calendar','isbn','product'o'driver_license', e il campo corrispondente viene compilato.manualInputpermette all'utente di digitare il codice. - Sul web disegna un proprio livello fotocamera a schermo intero (pulsante di chiusura, il gesto indietro lo chiude) e decodifica con
BarcodeDetector; lì sono compilati solovalueeformat, e il browser chiede l'autorizzazione alla fotocamera. nullsignifica sempre che l'utente ha chiuso lo scanner; ogni altro esito viene rifiutato con uncode, quindi mostra un messaggio incatch.- Richiamalo da un tocco, mai al caricamento della pagina. Restringi
formatsquando li conosci.
appmintNative.scheduleAlarm appmintNative#
scheduleAlarm(id, input) — @/lib/appmintNative
Esempio
Imposta una vera sveglia da un'app creata con l'AI: suona al volume della sveglia del telefono (anche in modalità silenziosa e con Non disturbare quando l'utente consente le sveglie), continua a suonare finché non premi Chiudi / Posticipa o ringSeconds, e porta l'app sullo schermo sopra la schermata di blocco.
Restituisce: boolean, in modo sincrono - true quando è impostata; false sul web, su iOS, o quando alarmSupport() non è 'ready'. Lo stesso id sostituisce una sveglia precedente. Richiede: 🔔 Alarm clock nel passaggio Accesso (Passaggio 4) quando crei l'app.
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';
}Offri la sveglia solo quando l'app può farne suonare una e spiega cosa manca in caso contrario:
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';
}Reagisci a come è finita una sveglia che suonava e fermala dalla tua schermata:
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
};
}Note: solo app Android. repeat giornaliera / settimanale / giorni feriali / mensile / annuale mantengono lo stesso orario anche con il cambio ora legale. Annulla con cancelNotification(id) sul bridge; gli esiti arrivano tramite onAlarm.
appmintNative.ScreenOrientation appmintNative#
ScreenOrientation.lock({ orientation }) / ScreenOrientation.unlock()
Esempio
Blocca lo schermo in verticale o in orizzontale per una vista, nella forma del plugin ScreenOrientation di Capacitor: lo stesso codice di lockOrientation().
Restituisce: Promise<void> per entrambe. lock viene rifiutata, come quella di Capacitor, quando orientation non è un tipo di blocco o la piattaforma rifiuta il blocco. Richiede: niente.
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" />;
}Blocca per una sola schermata solo quando un blocco è consentito e lascia che l'utente ruoti di nuovo:
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>;
}Quando ti serve sapere se il blocco ha funzionato, usa direttamente lockOrientation() (risponde false dove il blocco è rifiutato, come nei 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');
}Note: orientation è uno dei tipi di blocco di Capacitor - 'any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'portrait-secondary', 'landscape-primary', 'landscape-secondary' - e viene applicato così com'è. I browser desktop rifiutano i blocchi; i browser dei telefoni di solito li permettono solo a schermo intero. lockOrientation(to) restituisce true/false. Dai all'utente una via d'uscita da una vista bloccata.
appmintNative.screenReader appmintNative#
screenReader.isEnabled() / screenReader.speak(text, lang?) / screenReader.onChange(cb)
Esempio
Dice all'app se TalkBack è attivo e annuncia brevi messaggi.
Restituisce: isEnabled() → Promise<boolean> (false sul web: non esiste alcuna API web). speak() → Promise<void>. onChange(cb) → una funzione per annullare l'iscrizione; cb(enabled) viene eseguita quando il lettore di schermo viene attivato o disattivato (solo nell'app). Richiede: niente.
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');
}Annuncia un risultato solo a chi usa il lettore di schermo e lascia i toast visivi a tutti gli altri:
import { screenReader, toast } from '@/lib/appmintNative';
export async function notifySaved() {
if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
else toast('Note saved');
}Rimuovi un'animazione mentre TalkBack è attivo e smetti di ascoltare quando il componente sparisce:
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>;
}Note: Nell'app speak passa dal plugin ScreenReader; sul web (o se il plugin fallisce) viene letto con la sintesi vocale. Per la maggior parte degli annunci preferisci un buon markup (regioni aria-label, aria-live); usa speak per cambiamenti brevi e importanti.
appmintNative.searchContacts appmintNative#
searchContacts(query: string, limit = 50): Promise<Contact[]>
Esempio
Trova i contatti che corrispondono a una parola di ricerca. Solo app.
Restituisce: Promise<Contact[]> - vuoto quando nulla corrisponde, l'accesso è negato o Contatti è disattivato. Richiede: lo stesso di listContacts: Contatti nel Passaggio 4 (Accesso) (attivato dalla build quando il tuo codice chiama questa funzione) e l'autorizzazione dell'utente al primo utilizzo.
Campo di ricerca con un breve ritardo durante la digitazione.
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);
}Mostra uno stato chiaro quando Contatti non è disponibile e quando nulla corrisponde:
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>
);
}Cerca una persona per nome, poi precompila un messaggio per lei:
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
}Note: La corrispondenza è fatta dalla ricerca contatti del telefono sui nomi. Ogni contatto è { id?, name?, phones?: string[], emails?: string[], photoUri? }. Sul web can('contactList') è 'none': non esiste una API web per leggere la rubrica. I contatti sono dati personali: dichiarali nel modulo Sicurezza dei dati di Play e nella tua informativa sulla privacy.
appmintNative.SecureKeyError appmintNative#
class SecureKeyError extends Error { code } — from '@/lib/appmintNative'
Esempio
L'errore lanciato da secureKeys. Il suo code è una breve parola su cui puoi ramificare, uguale nell'app e sul web.
Restituisce: un Error con name === 'SecureKeyError' e code: 'no-such-key', 'auth-required' (sblocca e riprova), 'key-invalidated' (i dati biometrici o il blocco schermo sono cambiati: i dati sono persi), 'strongbox-unavailable', 'disabled' (Secure Keys non presente in questa build / nessun Web Crypto), 'bad-alias', 'bad-payload' (non è base64 valido o è troppo corto), 'too-large' (oltre 4 MB in una sola chiamata su Android), 'foreign-origin' (l'ha chiesto una pagina di un altro sito), 'failed' (qualsiasi altro caso). Richiede: nulla.
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;
}
}
}Trasforma il codice in un breve messaggio per l'utente in un unico punto:
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 + ')';
}
}Tratta un payload manomesso o errato come un esito normale e cancellalo, ma considera una chiave mancante un errore da mostrare:
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;
}
}Note: e.message è code oppure "code: detail"; ramifica su e.code, mai sul messaggio. Le parole di errore proprie delle piattaforme vengono trasformate in questi codici e il loro testo originale resta nel messaggio. Un payload modificato o estraneo è 'bad-payload' sul web e 'failed' nell'app, che non riesce a distinguerlo da altri errori di cifratura.
appmintNative.secureKeys appmintNative#
secureKeys — generate / encrypt / decrypt / remove / has / list / isHardwareBacked
Esempio
Chiavi di cifratura che il dispositivo custodisce e che l'app non può mai esportare: l'Android Keystore nell'app e una chiave Web Crypto non estraibile sul web. Rimuovere la chiave è l'unica vera «eliminazione sicura».
Restituisce: tutte Promise. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked}; encrypt(alias, text) → stringa base64; decrypt(alias, base64) → il testo; remove(alias) / has(alias) / isHardwareBacked(alias) → booleano; list() → string[]. generate, encrypt e decrypt lanciano SecureKeyError con un code. Richiede: Secure Keys (encryption) nel Passaggio 4 (Accesso) quando crei l'app.
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;
}
}Elimina davvero - nessuno potrà leggere i dati sigillati dopo questo:
import { secureKeys } from '@/lib/appmintNative';
export async function wipeDiary() {
await secureKeys.remove('diary');
localStorage.removeItem('diary');
}Elenca le chiavi che l'app conserva e mostra se ciascuna è protetta dall'hardware (il web risponde sempre 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'}`));
}Note: can('secureKeys') è 'native' nell'app e 'web' in un browser, dove isHardwareBacked() è sempre false - non parlare mai di «hardware» lì. Una chiave requireAuth: nell'app encrypt/decrypt generano auth-required senza mostrare nulla una volta scaduta la finestra di sblocco - chiama unlock() e riprova; sul web è il runtime a mostrare unlock() da sé. Cifra un record alla volta (l'app limita una chiamata a 4 MB). Alias: lettere, cifre, _ - ., massimo 64.
appmintNative.sendSms appmintNative#
sendSms(phoneNumber: string, message: string): Promise<boolean>
Esempio
Invia un SMS direttamente, senza schermata di conferma. Solo app Android.
Restituisce: Promise<boolean> - true quando il telefono segnala il messaggio come inviato, false quando è fallito, quando gli SMS non erano abilitati o quando l'utente ha rifiutato l'autorizzazione. Richiede: l'interruttore SMS (read & send) nel Passaggio 4 (Accesso) - la build lo attiva quando il tuo codice lo richiama - e l'autorizzazione dell'utente al primo uso.
Invia un avviso e di' all'utente cosa è successo.
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';
}
}Chiedi prima una conferma, perché il messaggio parte senza una schermata per annullarlo:
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.');
}Invia lo stesso avviso a più persone, una alla volta, e conta cosa è partito:
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 };
}Note: Il wrapper si risolve alla PRIMA risposta, che è sent (il messaggio ha lasciato il telefono), non delivered. Per le conferme di consegna usa il WebToApk.sendSms grezzo con l'evento appmint:sms. can('smsSend') è 'none' sul web (nessun browser può inviare un SMS). Ogni SMS può costare denaro all'utente. SMS è un'autorizzazione con restrizioni di Google Play: serve una Dichiarazione sulle autorizzazioni approvata in Play Console, quindi preferisci composeSms() quando l'utente può premere invia.
appmintNative.setInAppPresentation appmintNative#
import { setInAppPresentation } from '@/lib/appmintNative'
Esempio
Sceglie chi disegna un messaggio in-app (le campagne del creatore): l'app ('auto', il predefinito: il design a banner, scheda, pannello o schermo intero del creatore) oppure la tua interfaccia ('none', tramite onInAppMessage).
Restituisce: boolean in modo sincrono: true quando salvato (resta sul telefono). Sempre false sul web e senza AppMint Push. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app.
import { setInAppPresentation } from '@/lib/appmintNative';
// The app draws its own messages from now on.
setInAppPresentation('none');Un interruttore nelle impostazioni:
import { setInAppPresentation } from '@/lib/appmintNative';
export function onNativePopupsChange(on: boolean) {
if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}Prendi il controllo mentre una schermata è aperta, poi restituiscilo:
import { useEffect } from 'react';
import { setInAppPresentation } from '@/lib/appmintNative';
export function useQuietScreen() {
useEffect(() => {
setInAppPresentation('none');
return () => { setInAppPresentation('auto'); };
}, []);
}Note: un messaggio consegnato al tuo codice conta come mostrato. Impostare 'none' chiude anche quello attualmente a schermo.
appmintNative.setMusicVolume appmintNative#
setMusicVolume(percent) — @/lib/appmintNative
Esempio
Imposta il livello della musica di sottofondo in loop dell'app, da 0 a 100 (arrotondato e limitato automaticamente).
Restituisce: void. Richiede: un file scelto in Musica di sottofondo nel Passaggio 3 (Integra) quando crei l'app. Non fa nulla quando musicVolume() è -1.
L'app conserva il livello per il prossimo avvio, quindi basta un interruttore per il muto:
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';
export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40); // remembered by the appUn cursore del volume che compare solo quando l'app include musica:
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); }} />
);
}Abbassa la musica mentre un video è in riproduzione e ripristinala dopo:
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; }Note: 0 rende la musica silenziosa ma continua a riprodursi. Il livello viene salvato sul telefono (finché l'utente non cancella i dati dell'app). Solo app Android; sul web non succede nulla.
appmintNative.setPushPresentation appmintNative#
import { setPushPresentation } from '@/lib/appmintNative'
Esempio
Sceglie chi disegna un messaggio AppMint Push inviato con uno stile in-app (banner, pop-up, bottom sheet o schermo intero) mentre l'app è aperta: la shell dell'app ('auto', il valore predefinito) o il tuo codice ('none').
Restituisce: boolean in modo sincrono - true quando la modalità è stata salvata. Sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app.
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();
}Controlla la risposta, perché è false sul web o quando AppMint Push è disattivato:
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;
}Disegna un tuo banner per ogni messaggio e tieni la posta in arrivo allineata:
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); } };
}Note: la posta in arrivo e l'evento appmint:push funzionano allo stesso modo in entrambe le modalità. Impostare 'none' chiude anche un messaggio che la shell sta mostrando in questo momento.
appmintNative.setPushTag appmintNative#
import { setPushTag } from '@/lib/appmintNative'
Esempio
Imposta un tag AppMint Push su questa installazione. Il centro di invio può inviare a tutti quelli con un tag, e un'automazione basata sui tag può inviare un messaggio a un tempo stabilito dopo l'impostazione del tag. '' rimuove il tag.
Restituisce: boolean in modo sincrono - true quando salvato; false quando la chiave non è fatta di 1-32 lettere, cifre o _, il valore supera i 128 caratteri o l'installazione supererebbe i 50 tag. Sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app; il targeting per tag e le automazioni raggiungono le app create dal 30 set 2026 in poi.
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', '');
}Controlla il risultato: false significa che il tag è stato rifiutato (chiave non valida, valore oltre 128 caratteri, più di 50 tag) oppure che sei sul web, quindi tieni un tuo registro dello stato.
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;
}Etichetta un telefono una volta raggiunto un traguardo, così il centro di invio può raggiungere tutti quelli a quel livello.
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]);
}Note: i tag appartengono all'installazione, non all'utente che ha effettuato l'accesso. Non inserire mai dati privati in un tag.
appmintNative.setPushTags appmintNative#
import { setPushTags } from '@/lib/appmintNative'
Esempio
Imposta più tag AppMint Push in una sola modifica; un valore '' rimuove quel tag, i tag che non nomini vengono mantenuti.
Restituisce: boolean in modo sincrono - true quando salvato; false quando una chiave o un valore viola le regole di setPushTag (allora non viene salvato nulla). Sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app; il targeting per tag e le automazioni arrivano alle app create dal 30 set 2026 in poi.
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('');
}Etichetta un telefono con la lingua e gli argomenti scelti quando l'utente salva le sue impostazioni, e controlla che ogni tag sia stato accettato.
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;
}Mantieni i tag allineati all'account con accesso effettuato in un effect React, rimuovendo quelli che non servono più.
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]);
}Note: di una chiamata viene salvato tutto o niente.
appmintNative.setPushUser appmintNative#
import { setPushUser } from '@/lib/appmintNative'
Esempio
Dice ad AppMint Push quale dei tuoi utenti ha effettuato l'accesso su questa installazione, così il centro di invio può mandare un messaggio a quel solo utente. Passa il tuo id account stabile dopo l'accesso e '' all'uscita.
Restituisce: boolean in modo sincrono - true quando l'id è stato salvato; false per un id con qualcosa di diverso da lettere, cifre e _ @ . : + - (fino a 128). Sempre false sul web. Richiede: Android: AppMint Push attivato nel passaggio Integra (Passaggio 3) quando crei l'app; l'invio a un utente raggiunge le app create dal 30 set 2026 in poi.
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();
}, []);
}Gestisci un ID rifiutato: setPushUser risponde false per un indirizzo email o per qualsiasi ID con caratteri fuori dall'insieme consentito, quindi invia invece un ID stabile.
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;
}Cancella l'ID quando l'utente esce dall'account, in un semplice gestore di clic.
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>
);
}Combinalo con un tag, così un solo passaggio di accesso identifica insieme l'utente e il suo piano.
import { setPushUser, setPushTag } from '@/lib/appmintNative';
export function onSignedIn(userId: string, plan: 'free' | 'pro') {
setPushUser(userId);
setPushTag('plan', plan);
}Note: l'ID appartiene all'installazione, quindi l'uscita dall'account deve cancellarlo. Non cancella i tag - chiama setPushTags con valori '' per quelli specifici dell'utente.
appmintNative.setStatusBar appmintNative#
setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void
Esempio
Colora la barra di stato del sistema in base alla schermata e sceglie icone chiare o scure.
Restituisce: niente. Richiede: niente.
style è il colore delle ICONE: 'light' = icone chiare per una barra scura (predefinito), 'dark' = icone scure per una barra chiara. Chiamalo una volta all'avvio con il --bg dell'app, e di nuovo in ogni schermata con uno sfondo diverso:
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;
}Una schermata chiara:
import { setStatusBar } from '@/lib/appmintNative';
setStatusBar('#FFFFFF', 'dark');La forma stile Capacitor (lo stile prende il nome dallo SFONDO: 'DARK' = icone chiare):
import { StatusBar } from '@/lib/appmintNative';
await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });Note:
- Nell'app Android la shell colora la barra da sola, e funziona anche con Android 15 edge-to-edge; un colore non interpretabile viene ignorato - passa
#RRGGBB. Una chiamata solo di stile (backgroundColor = null) mantiene il colore impostato per ultimo da questa pagina e inverte le icone; prima che la pagina abbia impostato un colore inverte le icone tramite il plugin StatusBar e la barra mantiene il colore scelto in fase di build. - Sul web imposta
<meta name="theme-color">; il browser sceglie da sé il colore delle icone.
appmintNative.setVolume appmintNative#
setVolume(stream, percent, showUi?) — @/lib/appmintNative
Esempio
Imposta il livello del volume del telefono per un flusso, da 0 a 100. showUi mostra il cursore del volume del telefono mentre si muove.
Restituisce: { ok, stream?, current?, max?, percent?, error? } - l'errore è unknown_stream, bad_percent, fixed_volume, dnd_access_required (Non disturbare blocca suoneria / notifica: richiama requestDndAccess()), muted (Non disturbare «Silenzio totale» disattiva il canale, quindi il livello non è cambiato), not_applied, foreign_origin, failed, oppure unavailable sul web. Richiede: nulla per multimedia, sveglia e chiamata.
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();
}}
/>
);
}Mostra il volume multimediale come cursore che segue i tasti hardware, usando 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))} />;
}Alza il volume del canale sveglia prima che suoni una schermata di sveglia e segnala perché non è cambiato.
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.sheet appmintNative#
sheet(opts: { title?, content, dismissible? }): { close(), element, closed }
Esempio
Apre un pannello modale dal basso con un vero comportamento da pannello: per un filtro, un editor o un'anteprima di dettaglio.
Restituisce: un handle: close() lo chiude, element è l'elemento del corpo (riempilo o aggiornalo), closed è una Promise<void> che si risolve quando il foglio si è chiuso, in qualunque modo. Richiede: nulla.
Opzioni: title (titolo facoltativo), content - un elemento, testo semplice (non HTML) o una funzione (host) => { … } che riempie il corpo; dismissible (predefinito true: il tocco sullo sfondo, il trascinamento verso il basso, Esc e il gesto indietro lo chiudono).
Contenuto DOM semplice:
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;Contenuto React, mostrato nel pannello con un 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,
)}
</>
);
}Un foglio che resta aperto finché l'utente non sceglie, usando dismissible: false e la 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;
}Note:
- Disegnato con i token dell'app (
--surface,--ink,--muted,--border), sopra l'area sicura, con un leggero tocco aptico all'apertura. - Trascinare verso il basso lo chiude se trascinato oltre 120 px o con un gesto rapido; il corpo scorre per primo quando non è in cima.
- Con
dismissible: falsesolo il tuoclose()lo chiude: dai all'utente un pulsante. - Per un semplice elenco di azioni usa
actionSheet().
appmintNative.shortcuts appmintNative#
shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()
Esempio
Aggiunge scorciatoie al menu di pressione prolungata dell'icona dell'app (scorciatoie dinamiche di Android). Un tocco apre l'app su quella route.
Restituisce: set() restituisce Promise<boolean> - true quando le scorciatoie sono state consegnate al sistema, false dove non ce ne sono (il web). clear() restituisce Promise<void>. Richiede: niente.
Impostali una volta all'avvio e instrada in base al link di apertura:
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]);
}Rimuovili (per esempio dopo l'uscita dall'account):
import { shortcuts } from '@/lib/appmintNative';
await shortcuts.clear();Apri la schermata corrispondente quando si tocca una scorciatoia mentre l'app è già in esecuzione, con 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]);
}Mostra le scorciatoie solo a un utente che ha effettuato l'accesso e gestisci il web, dove set() risponde 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]);
}Note:
- Al massimo 4 voci; quelle in più vengono scartate. Un
routesenza/iniziale lo riceve.idè facoltativo. - Android 7.1+; sulle versioni precedenti di Android la chiamata non fa nulla. I titoli vengono tagliati a 25 caratteri (etichetta breve) e 50 (etichetta lunga).
- Un tocco apre l'app con il link di avvio
appmint-shortcut://<package><route>, quindinew URL(u).pathnameè il tuo percorso. Leggilo conlaunchUrl()al montaggio e cononDeepLink()mentre l'app è in esecuzione. can('shortcuts')è'none'sul web: un browser non ha un'icona dell'app.
appmintNative.snoozeAlarm appmintNative#
snoozeAlarm(id?, minutes?) — @/lib/appmintNative
Esempio
Ferma la sveglia che suona e la fa suonare di nuovo dopo minutes (0 o omesso = il snoozeMinutes della sveglia, predefinito 10). Lo squillo di domani di una sveglia ripetuta non si sposta.
Restituisce: boolean - true quando stava suonando ed è stata posticipata. Richiede: 🔔 Sveglia nel passaggio Accesso (Passaggio 4) quando crei l'app.
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);
});Posticipa da un pulsante nella tua schermata della sveglia, usando onAlarm per sapere quando sta suonando.
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 risponde false quando non suona nulla, quindi controlla prima di promettere all'utente un secondo squillo.
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
Esempio
Legge il testo ad alta voce con il motore di sintesi vocale del telefono.
Restituisce: void - inizia a parlare (o si accoda dopo la frase in corso) e non dà alcun segnale di «terminato». opts: { lang?: string; rate?: number; pitch?: number } (valori predefiniti: lingua del motore, 1, 1). Richiede: niente.
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>
);
}Lingue e velocità diverse:
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 = normalPronuncia una breve conferma dopo un'azione e avvisa l'utente anche con del testo.
import { speak, toast } from '@/lib/appmintNative';
export function announceSaved(title: string) {
toast('Saved');
speak(`${title} saved`, { rate: 1.1 });
}Leggi un elenco una voce dopo l'altra; speak() mette in coda, quindi chiamala in un ciclo e interrompi tutto con stopSpeaking() quando la schermata si chiude.
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>;
}Note: Nell'app Android: il motore nativo dell'app. Sul web: speechSynthesis. Più chiamate a speak() vengono riprodotte una dopo l'altra. Se ti serve sapere quando il parlato finisce (per evidenziare parole o andare avanti), usa window.speechSynthesis direttamente con l'onend di un enunciato; funziona anche nell'app Android.
appmintNative.speech appmintNative#
speech — @/lib/appmintNative
Esempio
Dettatura: trasforma la voce dell'utente in testo. Il riconoscimento vocale di Android nell'app Android, la Web Speech API in Chrome.
Restituisce: speech.isAvailable() → Promise<boolean>. speech.start(onResult, opts?) → Promise<boolean>: true quando l'ascolto è iniziato, false quando l'autorizzazione al microfono è stata rifiutata o la dettatura non è disponibile. onResult(text, isFinal) viene richiamata con il testo. speech.stop() → Promise<void>. opts: { lang?: string; partial?: boolean; prompt?: string }. Richiede: Mic nel Passaggio 4 (Accesso); le build con AI lo attivano per te quando il codice usa il riconoscimento vocale.
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>
);
}Detta in un campo di testo e di' quando il microfono è stato rifiutato, dato che start() risponde 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>
);
}Smetti di ascoltare quando la schermata si chiude e rileggi il risultato con 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>
);
}Note:
- Con
partialattivo (il predefinito)onResult(text, false)arriva man mano che le parole vengono riconosciute, poi una voltaonResult(text, true)con il testo finale. Conpartial: falsericevi solo quello finale, quando l'utente smette di parlare. - La richiesta di autorizzazione viene mostrata al primo
start(). - Se l'utente non dice nulla,
onResultnon viene chiamata; l'ascolto semplicemente termina. Tieni nell'interfaccia un pulsante Stop o un timeout. - L'ascolto si ferma da solo dopo una pausa.
langusa per impostazione predefinita la lingua del telefono.
appmintNative.splash appmintNative#
splash.hold() / splash.ready()
Esempio
Mantiene la splash di avvio sullo schermo finché la prima vera schermata è pronta, invece di mostrare "splash, poi uno spinner".
Restituisce: niente (entrambe sono sincrone). Richiede: niente.
Per impostazione predefinita la splash si nasconde da sola alla prima visualizzazione dell'app: non serve nulla. Quando la prima schermata ha bisogno di dati, trattieni la splash a livello di modulo e rilasciala quando quella schermata è visibile:
// 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>;
}Trattieni la splash mentre viene ripristinata una sessione con accesso effettuato e rilasciala in un finally così un errore non la lascia visibile.
// main.tsx
import { splash } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';
splash.hold();
supabase.auth.getSession().finally(() => splash.ready());Rilascialo una volta sola, da un hook, dopo che i dati della prima schermata sono visibili.
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]);
}Note:
- Nell'app Android
hold()dice alla shell di mantenere la sua schermata iniziale;ready()la nasconde (il tempo minimo impostato dal creatore vale ancora). - Sul web entrambi non fanno nulla - non c'è schermata iniziale.
- La shell limita l'attesa a circa 6 secondi, quindi una pagina che non richiama mai
ready()si apre comunque.
appmintNative.StatusBar appmintNative#
StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })
Esempio
La barra di stato nella forma del plugin StatusBar di Capacitor, costruita su setStatusBar().
Restituisce: Promise<void> per entrambi. Richiede: nulla.
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' });Colore e icone insieme - preferisci setStatusBar(), che imposta entrambi in una sola chiamata:
import { setStatusBar } from '@/lib/appmintNative';
setStatusBar('#0B0F1A', 'light'); // dark bar, light icons
setStatusBar('#FFFFFF', 'dark'); // light bar, dark iconsCambia la barra con il tema chiaro e scuro dell'app, usando insieme le chiamate in stile 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]);
}Note: style segue Capacitor: 'DARK' indica uno sfondo scuro (icone chiare), qualsiasi altro valore indica icone scure. Nell'app Android è il bridge della shell a colorare la barra (la chiamata del colore del plugin non fa nulla su Android 15+), e setStyle() inverte le icone in base al colore attuale della barra. Sul web il colore diventa <meta name="theme-color">.
appmintNative.stopAlarm appmintNative#
stopAlarm(id?) — @/lib/appmintNative
Esempio
Ferma la sveglia che sta suonando, come il suo pulsante Ignora. Una sveglia ripetuta resta impostata per la volta successiva.
Restituisce: boolean - true quando stava suonando ed è stata fermata. id omesso = quella che sta suonando. Richiede: 🔔 Sveglia nel passaggio Accesso (Passaggio 4) quando crei l'app.
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>
);
}Ferma la sveglia dalla schermata che l'app apre quando la sveglia suona e reagisci a ogni modo in cui finisce:
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 ?? ''); },
};
}Controlla la risposta: false significa che non stava suonando nulla (oppure sei sul web), quindi non dire che si è fermato:
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
Esempio
Ferma subito la sintesi vocale e scarta tutto ciò che è ancora in attesa di essere letto.
Restituisce: void. Richiede: niente.
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>
);
}Leggi ad alta voce un testo lungo con la lingua e la velocità scelte dall'utente, e fermalo con un solo pulsante:
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>
);
}Interrompi la voce quando l'utente inizia a digitare una risposta:
import { stopSpeaking } from '@/lib/appmintNative';
export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}Note: Si può chiamare tranquillamente anche quando nulla viene letto. Nell'app Android: lo stop del motore nativo. Sul web: speechSynthesis.cancel().
appmintNative.swipeable appmintNative#
swipeable(el: HTMLElement, opts: SwipeOptions): () => void
Esempio
Aggiunge i gesti di scorrimento a un elemento - righe da eliminare scorrendo, cambio pagina, chiusura di una scheda.
Restituisce: una funzione per annullare l'iscrizione che ripristina anche il touch-action originale dell'elemento. Richiede: niente.
Opzioni: onLeft, onRight, onUp, onDown (almeno una), threshold (px di spostamento, predefinito 56), edgeGuard (px dal bordo sinistro/destro dello schermo in cui gli swipe orizzontali vengono ignorati, predefinito 24), haptics (tocco aptico di selezione su uno swipe, predefinito true).
Scorri a sinistra per eliminare, con un pulsante visibile per la stessa azione:
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>
);
}Passa tra le schede con sinistra / destra:
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();Chiudi una scheda scorrendola verso l'alto, con il gestore rimosso allo smontaggio:
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>;
}Note:
- Basato sui Pointer Events, quindi lo stesso codice funziona con tocco, penna e trascinamento del mouse.
- Imposta
touch-actionper te:pan-yper i gestori orizzontali (lo scorrimento verticale funziona ancora),pan-xper quelli verticali,noneper entrambi. - Uno scorrimento deve essere chiaramente in una direzione (1,5 volte di più su un asse); gli scorrimenti diagonali non scattano. Un secondo dito annulla il gesto.
- La protezione dei bordi esiste perché il gesto indietro di sistema di Android controlla i bordi dello schermo.
appmintNative.systemPalette appmintNative#
systemPalette(): Promise<SystemPalette>
Esempio
Legge i colori Material You del telefono (ricavati dallo sfondo su Android 12+), così l'accento dell'app può seguire lo sfondo dell'utente come le app di sistema.
Restituisce: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - ogni campione associa un tono ('0', '10', '50', '100' … '900', '1000') a un colore esadecimale. { available: false } sotto Android 12 e sul web. Richiede: niente.
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']);
}Usa i colori dello sfondo solo quando il telefono li offre e rileggili quando l'app torna sullo schermo:
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)
}Mostra l'interruttore "Abbina al mio sfondo" solo quando esiste una palette, usando { available: false } come risposta a "non qui":
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>;
}Note: Controlla il contrasto prima di usare un colore dello sfondo per testo o pulsanti: --accent deve comunque raggiungere 3:1 sul tuo sfondo. Offrilo come opzione («Abbina al mio sfondo»), non come tema forzato. Richiamalo di nuovo quando l'app riprende; l'utente potrebbe aver cambiato lo sfondo.
appmintNative.TextToSpeech appmintNative#
TextToSpeech — @/lib/appmintNative
Esempio
L'oggetto TextToSpeech in stile Capacitor (speak / stop), che gira sullo stesso codice di speak() e stopSpeaking().
Restituisce: speak({ text, lang?, rate?, pitch? }) → Promise<void> che si risolve quando il testo è stato pronunciato (o stop() lo ha interrotto) e viene rifiutata quando non è stato possibile pronunciarlo. stop() → Promise<void>. Richiede: niente.
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();
}Leggi ad alta voce un elenco di righe una dopo l'altra; ogni speak si risolve quando è in coda, così il motore le riproduce in ordine:
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' });
}
}Un pulsante di lettura ad alta voce che smette di leggere quando si lascia la schermata:
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>;
}Note: await TextToSpeech.speak(…) è il segnale di fine: il passaggio successivo dopo la lettura può seguirlo direttamente. Nell'app: il motore di sintesi vocale nativo Android della shell; sul web: speechSynthesis.
appmintNative.textZoom appmintNative#
textZoom.get() / textZoom.getPreferred() / textZoom.set(value)
Esempio
Un controllo della dimensione del testo nell'app. Nell'app cambia lo zoom del testo della pagina (1 = 100%); sul web cambia la dimensione del carattere di base, che ridimensiona ogni layout basato su rem.
Restituisce: get() → Promise<number>. getPreferred() → Promise<number> - la dimensione scelta dall'utente nelle impostazioni del telefono (1 sul web). set(value) → Promise<void>; il valore viene limitato a 0,5-3 e la promise viene rifiutata per un valore che non è un numero. Richiede: nulla.
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()); }Ricorda la scelta tra un avvio e l'altro:
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));
}Un selettore a passi per la dimensione del testo che legge il valore attuale, lo mantiene nell'intervallo consentito e lo mostra in percentuale:
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>
);
}Note: L'app segue già l'impostazione della dimensione del carattere del telefono; questo è un controllo in più, dentro l'app. Prova il layout con dimensioni grandi: le altezze fisse sui controlli di testo tagliano il contenuto.
appmintNative.toast appmintNative#
toast(message: string, opts?: { duration?: 'short' | 'long' }): void
Esempio
Mostra un breve messaggio che non richiede risposta: "Copiato", "Salvato", "Aggiunto alla lista".
Restituisce: niente (lancia e dimentica). Richiede: niente.
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>
);
}Un messaggio più lungo resta visibile per circa 3,5 secondi:
import { toast } from '@/lib/appmintNative';
toast('Your export will be ready in a minute', { duration: 'long' });La forma stile Capacitor Toast.show({ text, duration? }) fa lo stesso e restituisce Promise<void>:
import { Toast } from '@/lib/appmintNative';
await Toast.show({ text: 'Added to list' });Per un messaggio che l'utente DEVE vedere, usa invece una finestra di dialogo:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });Note:
- Nell'app Android il plugin Capacitor Toast lo mostra in fondo allo schermo.
- Ovunque altro è una piccola snackbar disegnata sopra la pagina con i colori dell'app (
--inksu--bg), sopra il margine dell'area sicura, conrole="status"così gli screen reader la annunciano. - Ne viene mostrato uno alla volta; un nuovo toast sostituisce il vecchio.
- Un toast è facile da perdere per natura. Non usarlo mai per un errore su cui l'utente deve intervenire.
appmintNative.Toast appmintNative#
import { Toast } from '@/lib/appmintNative'
Esempio
Documentato insieme a appmintNative.toast - l'esempio lì mostra anche questo.
Mostra un breve messaggio che non richiede risposta: "Copiato", "Salvato", "Aggiunto alla lista".
Restituisce: niente (lancia e dimentica). Richiede: niente.
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>
);
}Un messaggio più lungo resta visibile per circa 3,5 secondi:
import { toast } from '@/lib/appmintNative';
toast('Your export will be ready in a minute', { duration: 'long' });La forma stile Capacitor Toast.show({ text, duration? }) fa lo stesso e restituisce Promise<void>:
import { Toast } from '@/lib/appmintNative';
await Toast.show({ text: 'Added to list' });Per un messaggio che l'utente DEVE vedere, usa invece una finestra di dialogo:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });Note:
- Nell'app Android il plugin Capacitor Toast lo mostra in fondo allo schermo.
- Ovunque altro è una piccola snackbar disegnata sopra la pagina con i colori dell'app (
--inksu--bg), sopra il margine dell'area sicura, conrole="status"così gli screen reader la annunciano. - Ne viene mostrato uno alla volta; un nuovo toast sostituisce il vecchio.
- Un toast è facile da perdere per natura. Non usarlo mai per un errore su cui l'utente deve intervenire.
appmintNative.torch appmintNative#
import { torch } from '@/lib/appmintNative'
Esempio
La torcia del telefono: torch.on(), off(), toggle(), isOn(), isAvailable(). can('torch') è 'none' sul web.
Restituisce: on()/off() → Promise (rifiutata con code 'unavailable' o 'CAMERA_IN_USE'); toggle() → il nuovo stato; isOn() / isAvailable() → booleano. Richiede: nulla, nessuna autorizzazione.
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>
);
}Spegni la luce quando l'utente lascia la schermata e mostra l'errore segnalato dal telefono quando non si riesce a spegnerla:
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>;
}Fai lampeggiare la luce come segnale, poi lasciala spenta:
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));
}
}Note: si spegne quando l'app si chiude. Mentre il flusso della fotocamera dell'app è aperto, accendila invece con il vincolo torch della traccia.
appmintNative.trackEvent appmintNative#
import { trackEvent } from '@/lib/appmintNative'
Esempio
Segnala un momento della tua app così compaiono i messaggi in-app del creatore con il trigger «Quando accade un evento» (stesso nome). L'app scarica le campagne del creatore dal centro di invio AppMint Push e le mostra da sola.
Restituisce: boolean in modo sincrono: false per un nome non valido (lettere, cifre e _ . : -, fino a 64), sul web e nelle app create senza AppMint Push. Richiede: Android: AppMint Push attivato nel passaggio Integrazione (Passaggio 3) quando crei l'app.
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
}Segnala un cambio di vista che l'URL non mostra, così i messaggi "In una pagina" lo vedono:
import { trackEvent } from '@/lib/appmintNative';
export function selectTab(tab: 'home' | 'cart' | 'profile') {
setActiveTab(tab);
trackEvent('appmint:route', { path: '/' + tab });
}Un hook che traccia la prima volta che una schermata viene aperta:
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]);
}Note: i cambi di URL (caricamenti di pagina, pushState, route con #hash) vengono già rilevati; segnala solo ciò che l'indirizzo non cambia.
appmintNative.transition appmintNative#
transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>
Esempio
Esegue un cambio di schermata con un'animazione dall'aspetto nativo: push fa entrare la nuova schermata da destra, pop la fa tornare indietro, fade fa una dissolvenza incrociata.
Restituisce: Promise<void> che si conclude dopo il cambiamento e la sua animazione. update è il tuo cambio di stato (un navigate del router, un setState) e viene sempre applicato. Richiede: nulla.
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>;
}Dissolvenza tra due schede:
import { transition } from '@/lib/appmintNative';
declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });Usa una dissolvenza per il cambio di scheda, con un gestore per ogni scheda:
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>
);
}Note:
- Usa la View Transitions API quando la WebView la supporta. Altrimenti applica l'aggiornamento e riproduce una breve animazione di ingresso su
#root(o sul primo elemento figlio di<body>). - Quando l'utente ha chiesto al sistema di ridurre le animazioni, applica solo l'aggiornamento, senza animazione.
- Le animazioni durano circa 240 ms.
appmintNative.unlock appmintNative#
unlock(title?, subtitle?) — from '@/lib/appmintNative'
Esempio
Chiede al proprietario del telefono di dimostrare di essere presente (impronta, volto o PIN/codice del dispositivo) e ti dice se ci è riuscito.
Restituisce: Promise<boolean> - true quando sbloccato; false quando annullato, fallito, bloccato o quando su questo dispositivo nulla può verificare. Non lancia mai errori. Richiede: nell'app, Fingerprint nel Passaggio 4 (Accesso) quando crei l'app. Sul web: l'autenticatore di piattaforma del 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();
}Nascondi il pulsante dove non c'è nulla con cui sbloccare:
import { can } from '@/lib/appmintNative';
const showLockButton = can('unlock') !== 'none'; // 'native' | 'web' | 'none'Proteggi un'eliminazione con uno sblocco recente, poi conferma con un tocco aptico e un 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');
}Note: Nell'app il PIN, la sequenza o la password del dispositivo sono ammessi come secondo modo di accesso. Sul web la prima chiamata registra l'autenticatore della piattaforma del browser e conta come sbloccata. Questo dimostra che il proprietario del dispositivo è presente; NON è un accesso - non usarlo mai come unica protezione per dati che stanno su un server. Su Android 11 e successivi la richiesta si apre su qualsiasi telefono con blocco schermo - anche uno con solo PIN la riceve e si sblocca con il PIN; su Android 10 e precedenti deve prima essere registrata un'impronta, altrimenti risponde false senza richiesta.
appmintNative.updates appmintNative#
updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()
Esempio
Un aggiornatore lato pagina per gli aggiornamenti del bundle web. Il codice della pagina non ne ha bisogno: in un'app AppMint Remote Update viene eseguito in modo nativo dall'app Android, e updates.supported() restituisce false ovunque.
Restituisce: supported() → boolean (al momento sempre false); download(url, sha256, version, binaryVersion) → Promise<boolean> (false finché non è supportato); stage(dir, version, binaryVersion) → Promise<boolean>; confirm() → void; revertIfUnconfirmed(binaryVersion) → Promise<boolean>; current() → { path, version, binaryVersion } | null. Richiede: nulla per la tua app: gli aggiornamenti si pubblicano da Remote Update in AppMint.
L'unico uso sicuro nel codice dell'app è controllare e non fare nulla quando è disattivato:
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);
}Mostra quale bundle è in esecuzione (una riga «Informazioni» nelle impostazioni):
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';Mostra la versione dei contenuti in uso in una riga delle impostazioni, con l'etichetta integrata quando la pagina non ha un bundle in staging:
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>;
}Note:
- Nell'app Android il gestore Remote Update della shell scarica, verifica e distribuisce gli aggiornamenti e ripristina un bundle che non si carica. La pagina è servita dalla shell stessa, quindi un bundle preparato dal codice della pagina non verrebbe mai mostrato: per questo
supported()èfalse. - Sul web non c'è nulla da aggiornare; la pagina è ciò che invia il server.
- Gli aggiornamenti possono modificare solo HTML, CSS e JavaScript; una nuova funzione nativa richiede comunque una nuova build dell'app.
appmintNative.watchIdle appmintNative#
import { watchIdle } from '@/lib/appmintNative'
Esempio
Avvisa l'app quando l'utente diventa inattivo o blocca lo schermo, e quando torna (can('idle')). Avvialo da un tocco la prima volta: la richiesta di autorizzazione ne ha bisogno.
Restituisce: Promise della funzione di stop. La callback riceve { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } all'avvio e a ogni cambiamento. Viene rifiutata con code 'permission-denied' o 'unavailable'. Richiede: nulla, nessuna autorizzazione 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>
);
}Mostra uno stato di assenza e smetti di osservare allo smontaggio; un'autorizzazione rifiutata viene segnalata con il suo 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 };
}Metti in pausa un timer di polling mentre l'utente è assente e aggiorna quando torna:
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); };
}Note: la soglia è di almeno un minuto (il minimo previsto dalla specifica).
appmintNative.widget appmintNative#
widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()
Esempio
Riempie il widget della schermata Home dell'app: una scheda con un titolo, un valore grande, una didascalia, fino a dodici righe (ciascuna con colore, segnaposto e percorso facoltativi) e un tocco che apre l'app su un percorso.
Restituisce: set(card) → Promise<boolean> (true quando è salvata); clear() → Promise<void>; placedCount() → Promise<number> (quante copie ha posizionato l'utente); requestPin() → Promise<boolean> (true quando si è aperto il flusso «aggiungi widget» del launcher). Nessuno viene rifiutato. Richiede: nulla nell'app Android (il widget fa sempre parte della build).
Aggiorna la scheda dopo ogni modifica che l'utente vorrebbe vedere nella schermata iniziale:
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
});
}Un'agenda che riempie un widget ridimensionato, con sfondo trasparente e una riga in ritardo in rosso:
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',
});
}Un pulsante «Aggiungi alla schermata Home»:
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>
);
}Svuota la scheda quando l'utente esce dall'account e mostra quante copie ci sono nella schermata iniziale:
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>;
}Note:
- La scheda viene salvata sul dispositivo, sopravvive a un riavvio e ogni copia posizionata viene aggiornata.
placedCount()pari a0significa che nessuna copia è ancora sulla schermata Home. - Campi della scheda:
title(vuoto = il nome dell'app),value(mostrato grande inaccent, nascosto se vuoto),caption(testo piccolo sotto il valore),lines(max 12; sei sono sempre visibili, di più quando il widget è abbastanza alto; una riga è una stringa o{ text, color?, marker?, bold?, wrap?, route? }),route(predefinito/),textSize('auto'ingrandisce il testo con il widget finché ogni riga ci sta ancora, mandando a capo le righe lunghe su due righe;'small' | 'normal' | 'large' | 'xl'o un numero 0,6-3),align('start' | 'center' | 'end'),wrap(le righe lunghe occupano due righe), ebg/ink/accent. I colori usano per impostazione predefinita i token CSS--bg,--ink,--accent; devono essere#RRGGBB,#AARRGGBB(alpha PRIMA:#9914264Aè opaco al 60 %),#RGB,rgb()/rgba()o un nome di colore: un valorehsl()/oklch()ripiega sui colori predefiniti.set()restituiscefalseper untextSize/alignsconosciuto o un colore di riga non valido. - Un tocco apre l'app con
appmint-shortcut://<package><route>; leggilo conlaunchUrl()/onDeepLink(). requestPin()richiede Android 8+ e un launcher che lo supporti.clear()mostra la scheda vuota «Nulla da mostrare per ora».- Solo app Android:
can('widget')è'none'sul web, doveset()erequestPin()restituisconofalse.