API da ponte JavaScript
appmintNative (apps criados com IA)
Os wrappers tipados que todo projeto criado com IA importa de @/lib/appmintNative. Cada um escolhe o caminho nativo dentro do app e um caminho web, ou um none honesto, em um navegador - uma única chamada funciona em todo lugar.
appmintNative.actionSheet appmintNative#
actionSheet(title: string, options: { title: string; destructive?: boolean }[]): Promise<number | null>
Exemplo
Mostra uma folha inferior "escolha uma destas" - o controle nativo para o menu de ações de uma linha.
Retorna: Promise<number | null> - o índice da opção escolhida, ou null quando o usuário dispensou a folha (toque no fundo, Escape, cancelar do sistema). Dispensar é normal, não é um erro. Requer: nada.
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); }
}Abra a partir de um botão visível e de um toque longo na linha:
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>;
}A forma no estilo Capacitor ActionSheet.showActions({ title?, options }) recebe as mesmas opções e resolve { index }, com -1 para uma dispensa:
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)`Observaçþes:
- No app Android o plugin ActionSheet do Capacitor desenha a janela do sistema;
destructiveusa o estilo destrutivo dele. - Nos demais casos é uma folha real na paleta do app (
--surface,--ink,--danger), com fundo escurecido, Escape, foco movido para dentro da folha e um háptico de seleção. - Uma lista
optionsvazia resolvenullimediatamente. Para qualquer conteúdo que não seja uma lista de ações, usesheet().
appmintNative.ActionSheet appmintNative#
import { ActionSheet } from '@/lib/appmintNative'
Exemplo
Documentado junto com appmintNative.actionSheet - o exemplo de lá mostra este também.
Mostra uma folha inferior "escolha uma destas" - o controle nativo para o menu de ações de uma linha.
Retorna: Promise<number | null> - o índice da opção escolhida, ou null quando o usuário dispensou a folha (toque no fundo, Escape, cancelar do sistema). Dispensar é normal, não é um erro. Requer: nada.
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); }
}Abra a partir de um botão visível e de um toque longo na linha:
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>;
}A forma no estilo Capacitor ActionSheet.showActions({ title?, options }) recebe as mesmas opções e resolve { index }, com -1 para uma dispensa:
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)`Observaçþes:
- No app Android o plugin ActionSheet do Capacitor desenha a janela do sistema;
destructiveusa o estilo destrutivo dele. - Nos demais casos é uma folha real na paleta do app (
--surface,--ink,--danger), com fundo escurecido, Escape, foco movido para dentro da folha e um háptico de seleção. - Uma lista
optionsvazia resolvenullimediatamente. Para qualquer conteúdo que não seja uma lista de ações, usesheet().
appmintNative.addContact appmintNative#
addContact(contact: Contact): Promise<boolean>
Exemplo
Salva um novo contato na agenda do celular. Somente no app Android: a ponte do shell o salva diretamente (nenhuma tela de Contatos abre).
Retorna: Promise<boolean> - true quando salvo, false quando o usuário recusou a permissão de contatos ou o salvamento falhou. Rejeita com um Error cujo code é 'not_enabled' (a compilação não ativou a escrita de contatos) ou 'foreign_origin', e após uma espera de 5 minutos sem resposta. Requer: a opção Editar contatos na Etapa 4 (Acesso) - a compilação a ativa quando seu código chama isto - e a permissão do usuário no primeiro uso.
Salve um cliente a partir de um formulário.
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
}
}Pule um contato que já está salvo. searchContacts lê a agenda, então verifique antes de adicionar:
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';
}Distinga um erro de compilação de uma recusa. Uma rejeição com code 'foreign_origin' ou 'not_enabled' é um problema de compilação; false é a escolha do usuário:
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>;
}Observações: Salva imediatamente - nenhum editor de contato abre - então confirme com o usuário na sua própria tela antes. Salva os números como números de celular e o contato no dispositivo (sem conta). Gravar contatos é sensível: declare-o no seu formulário de Segurança dos dados do Play e na política de privacidade.
appmintNative.alarmSupport appmintNative#
alarmSupport() — @/lib/appmintNative
Exemplo
Se scheduleAlarm consegue definir um alarme agora e, se não, o que está faltando.
Retorna: 'ready', 'not_enabled' (compilado sem Despertador), 'needs_exact_alarm' (Android 12+: alarmes exatos estão desativados), 'needs_notification_permission' (Android 13+) ou 'unavailable' (web e iOS - não desenhe controles de alarme ali). Requer: 🔔 Despertador na etapa Acesso (Etapa 4) ao compilar.
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>;
}Defina um alarme diário somente quando o celular estiver pronto e diga por quê quando não estiver:
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.';
}Oculte a tela de alarme na web e reaja quando um alarme terminar, combinando com 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)
Exemplo
O nome, o id do pacote e a versão do app, uma forma de fechar o app e os eventos do app - no formato do plugin App do Capacitor.
Retorna: getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → o formato do Capacitor (uma Promise do handle que também tem remove()). Eventos: appStateChange ({ isActive }), pause, resume (a página indo para / voltando do segundo plano), appUrlOpen ({ url }, um deep link — igual a onDeepLink), backButton (o toque ou gesto de voltar; seu handler decide — o app não fecha também). Requer: nada.
import { App } from '@/lib/appmintNative';
const info = await App.getInfo();
aboutLine.textContent = `${info.name} ${info.version}${info.build ? ` (${info.build})` : ''}`;Feche o app a partir de um item "Sair" que não seja o único caminho a seguir:
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();
}Mostre a versão do app em uma tela 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>;
}Observações: no app getInfo lê o plugin App do Capacitor; quando ele não existe usa o nome e a versão da ponte do shell (id e build ficam então ''), e na web document.title com versão vazia. exitApp usa o closeWindow do shell no app e não faz nada na web, então nunca faça de Sair o único caminho adiante. Para eventos de pausa/retomada e deep links veja Capacitor.Plugins.App, launchUrl() e onDeepLink().
appmintNative.appLauncher appmintNative#
appLauncher.canOpen(url): Promise<boolean> / appLauncher.open(url): Promise<boolean>
Exemplo
Abre outro app por link - whatsapp://send?text=…, tel:, mailto:, geo: ou um link https que o sistema entrega ao app dele - e pergunta antes se isso pode funcionar.
Retorna: canOpen() → Promise<boolean>. open() → Promise<boolean> (false quando nenhum app aceitou o link). Requer: nada.
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);
}Ligar ou enviar e-mail (sem verificação; todo celular tem um app para isso):
import { appLauncher } from '@/lib/appmintNative';
await appLauncher.open('tel:+15551234567');
await appLauncher.open('mailto:[email protected]?subject=Order%2042');Abra o app de mapas com um link geo: e avise quando nada o aceitou:
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.');
}Observações: No app, usa o plugin AppLauncher do Capacitor. No Android 11+, canOpen só enxerga os esquemas que o app declara (https, geo, google.navigation, tel, mailto, sms, whatsapp, intent, upi); para os outros, responde false mesmo com o app instalado - chame open() e verifique o resultado. Na web, um esquema personalizado não pode ser verificado, então canOpen é true apenas para http(s), e open navega até o link.
appmintNative.appLock appmintNative#
appLock({ timeoutSec?, title?, onLocked? }) — from '@/lib/appmintNative'
Exemplo
Bloqueia o app de novo quando o usuário volta depois de ele ter ficado um tempo em segundo plano: pede impressão digital / rosto / PIN (unlock()) e ativa a tela de privacidade para que o seletor de apps não mostre seu conteúdo.
Retorna: uma função que para o bloqueio (chame-a ao sair da conta ou ao desmontar). onLocked(true) dispara quando o app bloqueia, onLocked(false) após um desbloqueio bem-sucedido. Requer: o mesmo que unlock() - Android: Impressão digital marcada ao compilar.
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;
}No componente raiz: const locked = useAppLock(); if (locked) return <LockScreen />;.
Pare o bloqueio ao sair da conta, para que um usuário desconectado não precise desbloquear:
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;
}Ofereça o bloqueio apenas onde o dispositivo puder abri-lo e combine com unlock() para um botão manual "Bloquear agora":
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>
</>
);
}Observações: Se o dispositivo não tem nada para desbloquear (can('unlock') === 'none'), nada acontece e retorna uma função vazia - um bloqueio de app que nunca poderia abrir prenderia o usuário. Se o usuário cancela, ele pergunta de novo até cinco vezes e depois continua bloqueado (mantenha sua tela de bloqueio visível) até o app ir para segundo plano e voltar. Isto é separado da opção Bloqueio por PIN do assistente de compilação, que o app mostra nativamente ao iniciar.
appmintNative.appUpdate appmintNative#
appUpdate.check(): Promise<AppUpdateInfo | null> / appUpdate.start(): Promise<boolean>
Exemplo
Verifica no Google Play se há uma versão mais nova e inicia a atualização no app do Play (baixa e instala no lugar).
Retorna: check() → Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null na web, sem o Play Services ou quando o Play falha). updateAvailable é a resposta da própria loja. start() → Promise<boolean>: true somente quando a atualização do Play foi concluída, false quando o usuário cancelou, falhou ou não havia nenhuma disponível. Requer: nada; o app precisa estar instalado pelo 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);
});
}Compare o código de versão do Google Play com o instalado e trate null como "não é possível verificar aqui":
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}).`;
}Um banner React que verifica uma vez quando a tela abre:
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>;
}Observações: updateAvailable é a resposta do próprio Play; inPlace acrescenta que o Play pode executar sua atualização no app agora. availableVersion é o CÓDIGO de versão do Play (o Play não fornece o nome) - compare-o com currentVersionCode, nunca com currentVersion, que é o nome da versão.
appmintNative.askForReview appmintNative#
askForReview(options): Promise<ReviewOutcome>
Exemplo
Pede ao Google Play que mostre a própria tela de avaliação dentro do app e informa o que aconteceu.
Retorna: Promise<ReviewOutcome> — { result, openedStore, message? } com result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' na web). { openStore: true } abre a página do Google Play quando a janela não apareceu. Requer: nada; a janela só aparece para um app instalado pelo 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!');
}Diga ao usuário o que aconteceu em cada resultado:
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}` : ''}.`;
}
}Pergunte uma vez, depois que o usuário terminar a terceira tarefa, e lembre que você perguntou:
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();
}Observações: peça depois de um momento de sucesso e raramente. Uma instalação de teste fora da loja sempre responde not_from_play.
appmintNative.backgroundLocation appmintNative#
import { backgroundLocation } from '@/lib/appmintNative'
Exemplo
Localização que continua chegando enquanto o app está em segundo plano: start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.
Retorna: start(options) → Promise<BgResult> ({ ok, background, backgroundPermission, precise } ou { ok: false, reason }). stop / clearBuffered → Promise<boolean>. buffered() → Promise<LocationFix[]>. requestAlways() → Promise<{ foreground, background, precise, error? }>. onFix(cb) → uma função para cancelar a assinatura; cb recebe cada detalhe de appmint:location. Requer: Localização em segundo plano na compilação; permissão de localização (peça antes com 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(); };
}Peça "Permitir o tempo todo" antes de começar e explique uma recusa:
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}).`;
}Um hook React que mostra pontos ao vivo e para de escutar quando a tela fecha:
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'
Exemplo
Requisições HTTP nativas que rodam com o app fechado: enqueue (única, com salvamento automático durável enviado quando há conexão), schedule (uma requisição repetida, no mínimo a cada 15 minutos), cancel e list.
Retorna: enqueue / schedule → Promise<boolean>; cancel → Promise<void>; list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } — pendingDelivery no Android: uma tarefa única ainda não entregue). Requer: nada no app Android (usa o WorkManager). Na web can('backgroundSync') é 'none' e enqueue / schedule resolvem false.
Salvamento automático durável: o mesmo id substitui o salvamento que ainda não foi enviado.
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),
});
}Sincronização repetida, listar e cancelar:
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 bruto (o que o wrapper aguarda no 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);
});Observações: true significa que a tarefa foi enfileirada, não entregue. false significa que ela não foi enfileirada: o shell respondeu status: 'failed' (uma URL que não começa com http, um id vazio ou o sistema recusou) ou não respondeu em 10 segundos. O JavaScript da página nunca é executado em segundo plano; apenas esta solicitação. Uma resposta 4xx não é repetida; erros de rede e 5xx são repetidos até 8 vezes.
appmintNative.backgroundTasks appmintNative#
import { backgroundTasks } from '@/lib/appmintNative'
Exemplo
O código da própria página, executado periodicamente com o app fechado (wrapper tipado sobre AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.
Retorna: register / runNow → Promise<BgResult>; unregister / done → Promise<boolean>; list() → as tarefas registradas com lastRunAt / lastResult; isBackgroundRun() / currentRun() são síncronos. Requer: Executar código da página com o app fechado na compilação.
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();
}Registre ao entrar, remova ao sair e liste o que está registrado:
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.';
}Execute uma tarefa agora mesmo a partir de um botão "Sincronizar agora" e verifique a resposta:
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'
Exemplo
Define ou limpa a contagem de não lidas no ícone do app na tela inicial.
Retorna: badge.set(count) → Promise<boolean> (false quando o plugin Badge não existe, por exemplo na web); badge.clear() → Promise<void>. Requer: nada no Android (usa o plugin Badge embutido).
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();
}Espelhe a contagem de não lidas quando uma mensagem push chega com o app aberto:
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);
}Marque tudo como lido e zere a contagem juntos; pule o selo onde ele não existe:
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;
}Observações: no Android, a aparência depende do launcher: muitos mostram um número, alguns mostram apenas um ponto, alguns não mostram nada - não coloque informação somente no selo. Na web, can('badge') é 'none'.
appmintNative.barcodeFormats appmintNative#
barcodeFormats(): Promise<string[]>
Exemplo
Lista os formatos de código de barras e QR que o BarcodeDetector deste dispositivo consegue ler.
Retorna: Promise<string[]> - nomes de formatos como 'qr_code', 'ean_13', 'code_128'; um array vazio quando a leitura de código de barras não está disponível aqui. Nunca rejeita. Requer: nada.
Mostre a opção "Escanear código de barras do produto" apenas quando o dispositivo lê esse 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;
}Restrinja uma leitura ao que o dispositivo suporta:
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)));Crie um seletor de formato que liste apenas o que este dispositivo consegue ler:
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>
);
}Observaçþes:
- Ele consulta
BarcodeDetector.getSupportedFormats()do motor do navegador. O WebView do app Android tem esse recurso (medido: aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e); muitos navegadores de desktop e o Safari não têm, e respondem[]. - Esta lista trata de
readBarcodeFrom()/scanBarcode().scanCode()no app usa o scanner do Google e não depende disso.
appmintNative.batteryOptimization appmintNative#
import { batteryOptimization } from '@/lib/appmintNative'
Exemplo
A isenção da otimização de bateria: isIgnoring() e request().
Retorna: isIgnoring() → boolean. request() → Promise<string>: 'already_ignoring', 'dialog_opened' (opção de compilação "Pedir diretamente para ignorar a otimização de bateria"), 'settings_opened', 'unavailable' ou 'unsupported' fora do app Android. Requer: nada para a lista de configurações.
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;
}Mostre o estado atual em uma linha de configurações:
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>
);
}Peça a isenção antes de iniciar uma tarefa longa em segundo plano, junto com 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'
Exemplo
Sensores fitness Bluetooth padrão - frequência cardíaca, velocidade/cadência de ciclismo, potência, velocidade/cadência de corrida e bateria do sensor - com os números já interpretados, no app Android.
Retorna: todos os métodos retornam Promise<void> (setWheelCircumference retorna void). Todo o resto chega como eventos appmint:ble da janela com detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. Requer: Bluetooth, que a compilação ativa sozinha quando o código usa bleSensors. Em um navegador (pré-visualização web) não há ponte de sensores: startScan() responde com {kind:'error', error:'not_enabled'}, então trate esse erro. Para saber de antemão, teste can('bluetooth') === 'native' — não !== 'none': no Chrome can('bluetooth') é 'web' (Web Bluetooth), que bleSensors não 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>
);
}Controle a tela pelo app instalado.
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>;
}Pare a busca no primeiro sensor e acompanhe a velocidade do ciclismo.
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>;
}Observações: As buscas duram 15 segundos (stopScan() encerra uma antes); um sensor é conectado por vez. Códigos de erro: not_enabled, permission_denied, bluetooth_unavailable, bluetooth_off, scan_failed, invalid_address. A velocidade precisa de dois pacotes e do tamanho da roda (padrão 2096 mm). As mesmas mensagens também vão para window.onAppMintBle; ouvir o evento é mais simples. Para qualquer outro dispositivo Bluetooth, use navigator.bluetooth.
appmintNative.brightness appmintNative#
brightness.get() / brightness.set(value) / brightness.restore()
Exemplo
Aumenta o brilho da tela para um cartão de embarque ou QR code e depois devolve o controle ao sistema. Somente nativo — uma página web não controla a luz de fundo.
Retorna: get() → Promise<number | null> (0-1 enquanto o app substitui o nível; -1 quando o app segue o nível do sistema; null na web ou quando não pode ser lido). set(value) → Promise<boolean> (valor limitado a 0-1; false na web). restore() → Promise<void>. Requer: nada.
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 />;
}Lembre o nível, aumente o brilho e depois restaure.
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;
}Mantenha a tela clara e acordada enquanto um código estiver sendo exibido.
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]);
}Observações: Altera apenas a janela deste app, não a configuração de brilho do celular, e a alteração termina quando o app fecha. restore() define -1 (seguir o sistema). get() lê a alteração da janela, então é -1 até você chamar set() - não é o nível do sistema.
appmintNative.browser appmintNative#
browser.open(url, { toolbarColor? }) / browser.close() / browser.onClosed(cb)
Exemplo
Abre uma página em uma aba de navegador dentro do app (Chrome Custom Tabs) - para termos, páginas de ajuda ou uma página de login - e uma nova aba na web.
Retorna: open() → Promise<boolean> (false quando a aba não pôde abrir). close() → Promise<void>. onClosed(cb) → uma função para cancelar a inscrição; cb é executado quando o usuário fecha a aba (somente no app). Requer: nada.
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.'); }
}Um link de termos em um 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>
);
}Feche a aba pelo código e atualize quando o usuário voltar.
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
};
}Observações: toolbarColor usa por padrão o token --bg do app. A aba é um navegador separado - ela não compartilha os cookies nem o localStorage do app. close() fecha a aba a partir do código no app; na web não faz nada. onClosed nunca é disparado na web.
appmintNative.calendar appmintNative#
calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar
Exemplo
Adiciona, lista e exclui eventos na agenda do PRÓPRIO celular do usuário (o provedor de calendário do Android) e abre o app de agenda em uma data.
Retorna: requestPermission() → Promise<'granted' | 'denied'>. createEvent(input) → Promise<string | null> (o ID do evento; null quando o usuário recusou o acesso ao calendário - rejeita quando o próprio evento não pôde ser criado, por exemplo, datas inválidas). listEvents({ from, to }) → Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (horários em ms de época). deleteEvent(id) / openCalendar(date?) → Promise<boolean>. Apenas createEvent rejeita, e somente por um evento que não pôde ser criado. Requer: ative Calendário na Etapa 4 (Acesso) ao compilar - a compilação o marca para você quando vê calendar.createEvent(, calendar.listEvents( ou calendar.requestPermission( no código.
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);
}Listar esta semana, excluir, abrir:
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());Adicione ao calendário apenas quando for possível escrever.
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;
}Observações: Na web, createEvent baixa um arquivo .ics que o usuário abre no calendário (retorna o UID), listEvents retorna [] (can('calendarRead') é 'none'), e deleteEvent / openCalendar retornam false. Lembretes e eventos repetidos estão disponíveis em Capacitor.Plugins.Calendar, não por este wrapper.
appmintNative.camera appmintNative#
camera.take(opts?) / camera.pick(opts?)
Exemplo
Tira uma foto com a câmera ou escolhe fotos da galeria: o plugin Camera do Capacitor no app, um campo de arquivo com capture na web.
Retorna: take() → Promise<Photo | null> com { dataUrl, mime }; null ao cancelar ou se a permissão for recusada. pick() → Promise<Photo[]>, vazio ao cancelar. Requer: Câmera na Etapa 4 (Acesso) — a compilação a ativa quando o seu código chama camera.take.
Tire uma foto de perfil com a câmera frontal.
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;
}Escolha uma foto da galeria com take, ou várias com 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);Envie uma foto transformando a data URL em um 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 });
}Observações: opções: source ('camera' padrão, ou 'photos'), quality 0-100 (padrão 80), direction ('rear' padrão, ou 'front'), allowEditing. As fotos não são salvas na galeria. Uma permissão recusada resolve null / [], nunca gera erro — mostre uma dica curta para o usuário saber por que nada aconteceu. A câmera é sensível: declare-a no seu formulário de Segurança dos dados do Google Play.
appmintNative.can appmintNative#
can(cap: Capability): 'native' | 'web' | 'none'
Exemplo
Informa onde um recurso realmente vai funcionar na plataforma atual, para que o app desenhe apenas controles que funcionam.
Retorna: uma string, de forma síncrona: 'native' (a implementação do próprio app — a ponte do shell ou um plugin Capacitor embutido no app), 'web' (um padrão web real neste navegador) ou 'none' (indisponível aqui — não exiba o controle). Requer: nada.
Oculte um controle que não funciona nesta plataforma:
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>;
}Mude o texto conforme a plataforma:
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';Nomes de capacidades: 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.
Mostre uma dica em vez de um controle morto e transforme um recurso em um simples sim/não com 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';Observaçþes:
- Um plugin Capacitor embutido no app sempre responde
'native'; caso contrário, a ponte do shell é verificada e depois o padrão web. toast,actionSheet,dialog,statusBareswipetêm uma versão web de verdade, então não são'none'em um navegador.contactList,contactAdd,smsSend,smsList,callLog,shortcuts,shareTargetewidgetsão'none'na web.- Alguns recursos só existem quando a compilação os ativou (acesso a pastas, chaves seguras, controles de mídia, SMS, contatos). Pergunte a
can(), nãoisNativeApp(). - Um nome desconhecido retorna
'none'. Para um simples sim/não, usehas(cap).
appmintNative.clearPushInbox appmintNative#
import { clearPushInbox } from '@/lib/appmintNative'
Exemplo
Esvazia a caixa de entrada do AppMint Push neste celular. É permanente - não há como desfazer.
Retorna: boolean de forma síncrona — true quando limpo; sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
import { clearPushInbox, pushInbox } from '@/lib/appmintNative';
export async function onClearAll(confirmDelete: () => Promise<boolean>) {
if (!(await confirmDelete())) return pushInbox();
clearPushInbox();
return pushInbox(); // []
}Mostre Limpar apenas quando houver algo para limpar e informe o resultado:
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>
);
}Marque tudo como lido em vez de excluir, o que não perde nada:
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;
}Observações: as notificações da bandeja e o seu histórico de envios no AppMint não são alterados; novas mensagens continuam chegando.
appmintNative.click appmintNative#
click() — @/lib/appmintNative
Exemplo
Toca o som de toque do app uma vez, para os seus próprios botões.
Retorna: void. Requer: Som de toque ativado na Etapa 3 (Integrar) ao compilar. Na web, e em apps compilados sem Som de toque, não faz nada.
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>
);
}Deixe o usuário desligar o som nas suas próprias configurações:
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
}Reproduza a partir de uma linha da lista junto com um 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>;
}Observações: Com Seguir a configuração do celular (recomendado), o clique fica silencioso quando o usuário desativou os "Sons de toque" no celular. Com Sempre tocar a cada toque, todo toque já faz clique, então chamadas extras de click() não são necessárias. Somente Android (o shell WebToApk.playClick).
appmintNative.Clipboard appmintNative#
Clipboard.write({ string?, url? }) / Clipboard.read(): Promise<{ value, type }>
Exemplo
A área de transferência no formato do plugin Clipboard do Capacitor — o mesmo código de copy() / paste().
Retorna: write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value é '' quando a área de transferência está vazia). Requer: nada.
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
}
}Copie um código e confirme com um toast:
import { Clipboard, toast } from '@/lib/appmintNative';
export async function copyCode(code: string) {
await Clipboard.write({ string: code });
toast('Code copied');
}Escreva um link quando não houver texto e trate uma área de transferência vazia como nada para colar:
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
}
}Observações: no app usa o plugin Clipboard do Capacitor; em um navegador, navigator.clipboard. write envia apenas string (ou url quando string está vazio) — sem imagens. Em um navegador, read() pode gerar erro quando o usuário nega o acesso; no app, uma área de transferência vazia é '', não um erro. Pergunte a can('clipboardRead') antes de mostrar um controle Colar.
appmintNative.collapsingHeader appmintNative#
collapsingHeader(header: HTMLElement, opts?: { scroller?: HTMLElement | Window; range?: number }): () => void
Exemplo
Faz um cabeçalho de título grande reagir à rolagem como os nativos, publicando um valor de 0 → 1 que seu CSS pode usar.
Retorna: uma função de cancelamento que também remove a variável e o atributo. Requer: nada.
Define --collapse (de 0 no topo até 1 após range px de rolagem) no cabeçalho e adiciona data-collapsed quando está totalmente recolhido:
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); }Pare de escutar a partir de um script simples; isto também remove --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);Observaçþes:
- O padrão de
scrolleré a janela (uma página que rola inteira); o padrão derangeé 64 px. - O ouvinte de rolagem é passivo e atualiza uma vez por quadro de animação, então não deixa a rolagem lenta.
appmintNative.composeSms appmintNative#
composeSms(phoneNumber: string, message: string): boolean
Exemplo
Abre o app de mensagens com o número e o texto preenchidos; o usuário aperta enviar. Funciona no app Android (ponte do shell) e na web (um link sms:).
Retorna: boolean na hora — true quando o app de mensagens foi solicitado a abrir. Requer: nenhuma permissão.
Um botão "Enviar SMS para a loja".
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.');
});
}Um hook reutilizável que oculta o botão quando não há número.
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}.`),
};
}Convide um amigo; o usuário aperta enviar.
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');
}Observações: Prefira isto a sendSms() para qualquer coisa que o usuário escreve ou aprova - nada sai do celular sem o usuário apertar enviar. Na web, navega para sms:…?body=…. Uma chamada composeSms( não ativa SMS (ler e enviar) ao compilar, então não adiciona nenhuma permissão restrita do Play.
appmintNative.copy appmintNative#
copy(text: string): Promise<boolean>
Exemplo
Coloca texto na área de transferência do sistema.
Retorna: Promise<boolean> - true quando o texto foi copiado, false quando a área de transferência não está disponível ou o navegador recusou. Requer: nada.
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>
);
}A forma no estilo Capacitor faz a mesma coisa:
import { Clipboard } from '@/lib/appmintNative';
await Clipboard.write({ string: 'ABC-123' });Informe os dois resultados, com vibração apenas no sucesso:
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;
}Observações: No app, o plugin Capacitor Clipboard grava o texto (um erro do plugin rejeita a promise em vez de retornar false). No navegador, usa navigator.clipboard.writeText, que exige uma página https e, em geral, um toque do usuário.
appmintNative.copyImage appmintNative#
import { copyImage } from '@/lib/appmintNative'
Exemplo
Copia uma imagem para a área de transferência para colar em apps de chat e e-mail: a área de transferência do app no app Android, a Async Clipboard API na web.
Retorna: Promise<boolean> — true quando copiado. Requer: nada; condicione o controle 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');
}Copie uma foto da câmera e confirme com um 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');
}Mostre o botão somente onde a área de transferência aceita imagens e copie uma imagem a partir de uma 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>
);
}Observações: PNG é o tipo mais seguro em todo lugar; o app Android também aceita JPEG e WebP.
appmintNative.Device appmintNative#
Device.getInfo() / Device.getId() / Device.getLanguageCode()
Exemplo
O formato de @capacitor/device: modelo do celular, versão do SO, um ID estável e o idioma. Usa o plugin Device do Capacitor no app e a resposta do navegador na web.
Retorna: getInfo() → Promise<DeviceInfo & { operatingSystem, isVirtual }> (com Capacitor também manufacturer, webViewVersion, …); getId() → Promise<{ identifier }>; getLanguageCode() → Promise<{ value }> (duas letras, por exemplo "en"). Requer: nada.
Mostre o modelo e a versão do sistema em uma tela Sobre.
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' : '');
}Um ID estável e o código do idioma.
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 }),
});Mostre a versão do app e o modelo do dispositivo no rodapé das configurações. O relatório de sincronização fornece a versão do app; o plugin adiciona o sistema operacional.
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}`;
}Observações: No app, o deviceInfo() de sincronização também preenche model e osVersion; Device.getInfo() acrescenta operatingSystem, isVirtual e os campos extras do plugin. Na web, operatingSystem é 'unknown' e model fica vazio. getId() na web é o ID de localStorage de installId() e lança um erro quando o armazenamento está bloqueado.
appmintNative.deviceInfo appmintNative#
deviceInfo(): DeviceInfo
Exemplo
Informa de forma síncrona onde o app está rodando (platform) e o idioma, além do relatório completo do dispositivo do shell Android dentro do app Android.
Retorna: DeviceInfo imediatamente: sempre { platform, language } - platform é 'android' no app e 'web' em um navegador. No app Android, também preenche model, manufacturer, osVersion (ex.: "14") e appVersion (ex.: "1.4.0"), e traz todas as chaves de WebToApk.getDeviceInfo(): android, hardware, screen, app, runtime, installId. Na web, acrescenta osVersion (o user agent). Requer: nada.
Escolha o comportamento pela plataforma durante a renderização.
import { deviceInfo } from '@/lib/appmintNative';
const info = deviceInfo();
const isAndroidApp = info.platform === 'android';
const lang = info.language; // e.g. "en-IN"Leia detalhes do Android. Os campos comuns ficam no topo; o restante do relatório do shell permanece em seus grupos aninhados.
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}%`);
}Escolha o controle certo conforme a plataforma.
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>;
}Observações: model e appVersion são preenchidos apenas no app. Na web, osVersion é a string de user-agent do navegador, não uma versão do sistema. Lança uma exceção se a resposta do shell não for um JSON válido (um defeito real, nunca esperado).
appmintNative.Dialog appmintNative#
Dialog.alert / Dialog.confirm / Dialog.prompt
Exemplo
Diálogos reais do sistema: uma mensagem, uma pergunta sim/não, uma pergunta de texto de uma linha. O app usa o plugin Dialog do Capacitor; a web usa o próprio alert / confirm / prompt.
Retorna: 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 nunca rejeitam (dispensar é uma resposta); alert rejeita apenas quando não pode ser exibido (sem mensagem). Requer: nada.
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);
}Alerta e prompt:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Offline', message: 'Your changes will sync when you are back online.' });
const r = await Dialog.prompt({ title: 'Rename', message: 'New name', inputText: current, inputPlaceholder: 'List name' });
if (!r.cancelled && r.value.trim()) rename(r.value.trim());Confirme antes de sair da conta. Dispensar a caixa de diálogo conta como "não".
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');
}Observações: Dispensar uma confirmação dá { value: false }; dispensar um prompt dá { value: '', cancelled: true }. As caixas de diálogo bloqueiam a tela - use-as para decisões reais e toast() para mensagens do tipo "Salvo". prompt não repassa títulos de botão personalizados ao plugin.
appmintNative.dismissPushMessage appmintNative#
import { dismissPushMessage } from '@/lib/appmintNative'
Exemplo
Oculta uma mensagem da caixa de entrada do AppMint Push (deslizar para remover). Desfaça com restorePushMessage(id).
Retorna: boolean de forma síncrona - true se estava oculto. Sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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());
});
}Mostre a caixa de entrada com um tratamento de deslizar e um estado de desfazer, para que um deslize nunca seja 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>
);
}Oculte todas as mensagens lidas de uma vez e informe quantas foram ocultadas.
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;
}Observações: apenas a caixa de entrada deste celular muda; a notificação na bandeja e o seu histórico de envios permanecem.
appmintNative.docScanner appmintNative#
docScanner.scan({ pageLimit?, pdf?, quality? }): Promise<{ pages, pdfPath? } | null> / docScanner.isSupported(): Promise<boolean>
Exemplo
Abre o escâner de documentos do sistema - detecção de bordas, endireitamento, várias páginas - o escâner ML Kit do Google no app Android. As páginas voltam como data URLs JPEG.
Retorna: Promise<{ pages: string[]; pdfPath?: string } | null> - null quando o usuário cancela, o escâner não consegue iniciar ou na web. isSupported() → Promise<boolean>: se este celular pode escanear (Google Play services); false na web. Requer: nada para ativar (no Android, o escâner roda no Google Play services e não precisa de permissão de câmera).
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>
);
}Pergunte primeiro ao celular, porque um dispositivo mais antigo pode não ter o scanner, e depois mantenha as páginas.
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} />)}
</>
);
}Escaneie e depois envie a primeira página com o auxiliar de transferência de arquivos.
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;
}Observações: pageLimit 0 (padrão) = sem limite; quality 0,1-1 (padrão 0,85). Com pdf: true, pdfPath é um caminho de arquivo no cache do app, não uma URL que a página possa carregar. Data URLs são grandes — redimensione ou envie em vez de manter muitas em localStorage. Para uma foto simples use camera.take().
appmintNative.enableNotifications appmintNative#
enableNotifications(): Promise<EnableNotificationsResult>
Exemplo
Traz de volta um usuário que negou ou ignorou as notificações, a partir de um botão que ele tocou: a janela de permissão enquanto ela ainda pode ser exibida e, depois disso (app Android), as configurações de notificação deste app.
Retorna: Promise<{ state, changed, openedSettings }> — state é 'granted' | 'prompt' | 'blocked' | 'unavailable', changed é verdadeiro quando difere de antes da chamada, openedSettings é verdadeiro quando a tela de configurações foi usada. Nunca rejeita. Na web pede por Notification.requestPermission(). Requer: notificações na compilação (ficam ativas sempre que o app envia ou recebe notificações).
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';
});
}Observações: mostre o banner apenas para 'prompt' e 'blocked'; oculte-o para 'granted' e 'unavailable'. Peça a partir de um toque, nunca ao abrir.
appmintNative.fileTransfer appmintNative#
fileTransfer.download / fileTransfer.upload from '@/lib/appmintNative'
Exemplo
Baixa ou envia um arquivo com um callback de progresso. No app, a transferência roda de forma nativa (Capacitor FileTransfer); na web, usa fetch / XMLHttpRequest com o mesmo callback.
Retorna: download(url, fileName, opts?) → Promise<{ path?: string; blob?: Blob }> - path (um arquivo no cache do app) no app, blob na web (onde o arquivo também é salvo pelo navegador). upload(url, file, opts?) → Promise<{ responseCode: number; responseBody: string }>. upload resolve com qualquer status HTTP que o servidor responder (verifique responseCode), tanto no app quanto na web; ambos rejeitam quando a própria transferência falha (sem conexão, arquivo inválido). Requer: nada para ativar.
Baixe com uma barra de progresso:
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)}`);
}
}Envie um File a partir de um <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})`);
}Baixe com um estado de progresso e depois abra o arquivo, em um único 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 };
}Observações: fraction é 0 enquanto o servidor não informa o tamanho. No app, um download vai para a pasta de CACHE do app (não para os Downloads do usuário); para dar uma cópia ao usuário, use saveFile. Em um upload, o app primeiro copia o arquivo para o cache, então arquivos muito grandes precisam de tanto espaço livre. method (padrão 'POST') e fileName são opções de upload opcionais.
appmintNative.fileViewer appmintNative#
fileViewer.open(target) from '@/lib/appmintNative'
Exemplo
Abre um documento (PDF, imagem, arquivo do Office) no app visualizador do celular. Na web abre o endereço em uma nova aba.
Retorna: Promise<boolean> - true quando o visualizador foi aberto, false quando falhou (nenhum app para esse tipo, caminho inválido) ou o destino não pode ser aberto nesta plataforma. Requer: nada para ativar; usa o plugin FileViewer do Capacitor que acompanha o app.
Abra um documento a partir da 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.');
}Abra um arquivo que você baixou antes com fileTransfer.download (um caminho local no 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 webAbra um link a partir de um botão e avise o usuário quando nenhum visualizador conseguir abri-lo.
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>
);
}Observações: Um destino http(s) usa openDocumentFromUrl; qualquer outro é tratado como caminho local (file:// é removido). Na web, apenas destinos http(s):, blob: e data: abrem; um caminho local responde false.
appmintNative.folder appmintNative#
import { folder } from '@/lib/appmintNative'
Exemplo
Uma pasta inteira que o usuário autoriza uma vez e o app mantém entre as aberturas, com operações reais de arquivo dentro dela (no app: a ponte de pastas do shell).
Retorna: todos os membros são assíncronos — connected() → boolean, label() → string, request() → boolean (falso ao cancelar), disconnect() → void, list(path) → [{ name, uri, isDirectory, length, lastModified }], readText(path) → string | null (null = arquivo inexistente), writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean, stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. Requer: um app criado com IA ativa sozinho a opção Acesso nativo a pastas (SAF) quando o código usa folder; em uma compilação manual, marque-a na Etapa 4 (Acesso).
Conecte, mostre a pasta e desconecte:
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();
}Ler, gravar, listar, renomear e excluir — todo caminho é relativo à pasta autorizada ('' é a raiz dela):
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 folderFaça backup de uma nota na pasta autorizada e diga claramente quando nenhuma pasta estiver conectada.
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');
}Observações: can('folderAccess') é 'none' na web - não há equivalente de pasta persistente em um navegador de celular, então não exiba o recurso ali (use pickFile() / saveFile() para arquivos individuais). Chame request() somente a partir de um toque do usuário. .. e caminhos absolutos são recusados. A própria pasta autorizada nunca pode ser removida. stat() funciona para arquivos e subpastas (uma pasta tem isDirectory: true e size: 0); stat('') - a raiz - é null, use label() para ela. request() pode ser chamado de novo após um cancelamento.
appmintNative.fullscreen appmintNative#
fullscreen.enter() / fullscreen.exit() / fullscreen.isOn()
Exemplo
Oculta as barras do sistema (no app) ou entra em tela cheia no navegador (na web), para um vídeo, um jogo ou uma tela de leitura.
Retorna: enter() e exit() retornam Promise<void> e nunca rejeitam; isOn() retorna boolean de forma síncrona. Requer: nada.
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>
);
}Saia da tela cheia quando a visualização for fechada:
import { useEffect } from 'react';
import { fullscreen } from '@/lib/appmintNative';
export function useFullscreenWhileOpen() {
useEffect(() => {
void fullscreen.enter();
return () => { void fullscreen.exit(); };
}, []);
}Oculte as barras do sistema em uma tela de leitura e acompanhe o estado a partir de um botão.
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 };
}Observaçþes:
- No app, este é o tela cheia do próprio shell (as barras de status e de navegação se ocultam); está sempre disponível para a página.
- Na web, a Fullscreen API só funciona a partir de um toque do usuário. Um pedido recusado é ignorado silenciosamente, então leia
isOn()depois em vez de presumir.
appmintNative.geofences appmintNative#
import { geofences } from '@/lib/appmintNative'
Exemplo
Geofences que disparam com o app fechado e sobrevivem a reinicializações: add, remove, list, events, clearEvents, onEvent, available.
Retorna: add(spec) → Promise<BgResult> resolvido quando o Play services aceitou ou recusou ({ id, ok, reason? } - background_permission_required, location_off, too_many_geofences, …). remove / clearEvents → Promise<boolean>. list() → Promise<GeofenceSpec[]>. events() → Promise<GeofenceEvent[]> (últimos 100, incluindo os de quando o app estava fechado). onEvent(cb) → um cancelamento de inscrição. Requer: Geofences na compilação e "Permitir o tempo todo" (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)));Liste as geofences ativas e as transições registradas enquanto o app estava fechado.
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()}`),
];
}Adicione uma zona de trabalho para entrada e saída, trate uma recusa pelo motivo e remova-a depois.
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
Exemplo
O formato de @capacitor/geolocation: o plugin Geolocation do Capacitor no app, o navigator.geolocation padrão na web. Todos retornam o objeto de posição do W3C.
Retorna: getCurrentPosition(opts) → Promise<GeolocationPosition> (rejeita em caso de recusa, tempo esgotado ou falta de localização); watchPosition(opts, cb) → Promise<string> (o id da observação); clearWatch({ id }) → Promise<void>; checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (cada 'granted' | 'denied' | 'prompt'; no Android 12+, um usuário que escolhe "Aproximada" concede apenas coarseLocation). Requer: GPS / Localização na Etapa 4 (Acesso) - a compilação ativa os dois quando seu código chama getCurrentPosition ou watchPosition.
Onde estou, uma vez.
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?';
}
}Acompanhe um trajeto e depois pare. O callback recebe null e um erro quando uma leitura de posição falha.
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; }
}Obtenha uma posição pontual. Retorna null quando o usuário recusa.
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
}
}Observações: navigator.geolocation simples também funciona no app; use isto quando quiser o formato do Capacitor. No Android 12+, o usuário pode permitir apenas a localização aproximada - você ainda recebe uma posição, só que menos precisa. Na web, requestPermissions() faz uma solicitação real de posição para acionar o aviso do navegador. A localização é um dado pessoal: declare-a no seu formulário de Segurança dos dados do Play.
appmintNative.getNotificationState appmintNative#
getNotificationState(): Promise<NotificationState>
Exemplo
Se as notificações estão ativadas e, se não, se o app ainda pode pedir ou se apenas as Configurações podem resolver.
Retorna: Promise<'granted' | 'prompt' | 'blocked' | 'unavailable'>. Na web, Notification.permission mapeado da mesma forma (default → 'prompt', denied → 'blocked'); 'unavailable' onde não há notificações. Requer: nada.
import { getNotificationState } from '@/lib/appmintNative';
export async function shouldShowNotifBanner(): Promise<boolean> {
const state = await getNotificationState();
return state === 'prompt' || state === 'blocked';
}Observações: verifique de novo quando o app voltar ao primeiro plano (visibilitychange) - o usuário pode ter mudado isso nas Configurações.
appmintNative.getPushTags appmintNative#
import { getPushTags } from '@/lib/appmintNative'
Exemplo
Lê as tags do AppMint Push desta instalação como um objeto.
Retorna: Record<string, string> de forma síncrona - por exemplo, { plan: 'pro', cart: 'open' }; {} quando não há nenhuma e na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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');Defina várias tags em uma única alteração com setPushTags quando uma tela muda mais de uma, e esvazie um valor para remover a tag dele:
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)');
}Leia as tags de volta para mostrar uma dica apenas quando esta instalação tem uma 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>;
}Observações: isto é o que o celular guarda; a central de envios vê isso após o próximo check-in da instalação.
appmintNative.getPushUser appmintNative#
import { getPushUser } from '@/lib/appmintNative'
Exemplo
Lê o ID de usuário que esta instalação informou por último ao AppMint Push com setPushUser.
Retorna: string de forma síncrona - o id salvo, ou '' quando não há login, nunca foi definido, ou na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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);
}Chame setPushUser com o id da conta após o login e com '' ao sair; ele retorna false quando o id é recusado ou o AppMint Push está desativado:
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('');
}Leia o id salvo em um hook do React, junto com uma tag que pertence a esse usuário:
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;
}Observações: isto é o que o celular guarda; a central de envio vê a mudança após a próxima sincronização da instalação, alguns segundos depois quando está online.
appmintNative.getVolume appmintNative#
getVolume(stream) — @/lib/appmintNative
Exemplo
Lê o nível de volume do próprio celular para um fluxo: 'media', 'alarm', 'ring', 'notification' ou 'call'.
Retorna: { stream, current, max, percent, muted? } (current/max nos passos do próprio celular, percent 0-100), ou null na web. Requer: nada.
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 responde null na web, então desenhe o nível somente quando for um objeto:
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" />;
}Acompanhe as teclas físicas com onVolumeChange e aumente um nível baixo com 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
Exemplo
Uma vibração curta, com sensação nativa, para toques e resultados. Chame a cada toque relevante.
Retorna: void (dispare e esqueça). kind: 'light' (padrão), 'medium', 'heavy', 'selection', 'success', 'warning', 'error'. Requer: Vibrar na Etapa 4 (Acesso); as compilações com IA o ativam para você quando o código chama 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>
);
}Para abas, alternadores e seletores use 'selection'; para a confirmação de exclusão use 'heavy':
import { haptic } from '@/lib/appmintNative';
function onTabChange(tab: string) { haptic('selection'); setTab(tab); }
function onConfirmDelete() { haptic('heavy'); deleteItem(); }Um botão de curtir que dá um toque leve ao ativar e um mais firme ao desativar:
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>
);
}Observações: no app usa o plugin Haptics do Capacitor (impact, notification, selectionChanged). Na web usa navigator.vibrate onde o navegador tem, e não faz nada nos demais; nunca gera erro. Para ritmos mais longos use hapticPlay.
appmintNative.hapticPlay appmintNative#
hapticPlay(pattern) — @/lib/appmintNative
Exemplo
Toca um padrão de vibração rico para um momento especial (uma vitória, uma sequência, um batimento cardíaco). Use haptic() para toques normais.
Retorna: Promise<boolean>: true quando o padrão tocou, false quando não foi possível. Requer: Vibrar na Etapa 4 (Acesso).
Padrões nomeados: '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();
}Seu próprio padrão: uma lista de { 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
]);Combine com haptic(): um feedback de toque normal para cada resposta e o padrão longo apenas para o momento especial:
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
}
}Observações: o app usa o plugin Capacitor AppwrightHaptics (intensidade por batida onde o celular permite). A web recebe apenas o ritmo. sharpness é aceito, mas não tem efeito no Android. haptic.play(pattern) é a mesma função.
appmintNative.Haptics appmintNative#
Haptics — @/lib/appmintNative
Exemplo
O objeto Haptics no estilo Capacitor: os mesmos nomes e opções de @capacitor/haptics, rodando no mesmo código de haptic().
Retorna: todo método retorna void (não uma Promise; await nele é inofensivo). Requer: Vibrar na Etapa 4 (Acesso); compilações com IA o ativam para você quando o código 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 300Um seletor que dá um toque a cada mudança de valor:
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>
);
}Um formulário que confirma com uma vibração de notificação e adiciona uma vibração curta quando um campo está errado:
import { Haptics } from '@/lib/appmintNative';
export function onSubmit(valid: boolean) {
Haptics.notification({ type: valid ? 'SUCCESS' : 'ERROR' });
if (!valid) Haptics.vibrate({ duration: 200 });
}Observações: selectionStart e selectionChanged dão um único toque de seleção; selectionEnd não faz nada. Funciona no app (plugin Haptics do Capacitor); na web usa navigator.vibrate quando disponível e, caso contrário, não faz nada.
appmintNative.has appmintNative#
has(cap: Capability): boolean
Exemplo
A forma sim/não de can(): verdadeiro quando o recurso fará algo de fato nesta plataforma.
Retorna: boolean, de forma síncrona - exatamente can(cap) !== 'none'. Requer: nada.
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>
);
}Controle o acesso a um recurso inteiro:
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;
}Escolha um rótulo conforme onde roda: has() decide se desenha o controle, can() diz de que tipo ele é:
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';
}Observações: Aceita os mesmos nomes de recursos que can() (veja essa entrada para a lista completa). Use can() quando precisar saber ONDE roda ('native' ou 'web'), por exemplo para mudar um rótulo.
appmintNative.hideKeyboard appmintNative#
hideKeyboard(): void
Exemplo
Fecha o teclado na tela - por exemplo, quando um formulário é enviado ou uma folha abre sobre o campo.
Retorna: nada. Requer: nada.
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>
);
}Feche o teclado quando uma folha abre sobre o campo, para que a folha não fique escondida atrás dele:
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),
};
}Feche quando o usuário tocar em qualquer lugar que não seja um campo de texto:
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>
);
}Observações: no app pede ao plugin Keyboard do Capacitor para ocultar o teclado. Na web tira o foco do campo ativo, que é o que fecha o teclado em um navegador. É seguro chamar quando nenhum teclado está aberto.
appmintNative.inAppCampaigns appmintNative#
import { inAppCampaigns } from '@/lib/appmintNative'
Exemplo
As mensagens dentro do app que esta instalação guarda agora, como {id, name}[]: as campanhas do criador para as quais este usuário está no público.
Retorna: { id: string; name: string }[] de forma síncrona; [] na web e sem o AppMint Push. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
import { inAppCampaigns } from '@/lib/appmintNative';
console.log(inAppCampaigns().map((c) => c.name));Uma lista de depuração em uma tela de administração:
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>;
}Pule a sua própria dica quando o criador já tem uma mensagem com esse nome em andamento:
import { inAppCampaigns } from '@/lib/appmintNative';
export const hasCampaign = (name: string) => inAppCampaigns().some((c) => c.name === name);appmintNative.installId appmintNative#
installId(): string
Exemplo
Fornece um ID aleatório estável para esta instalação: o ID do próprio shell no app Android, um ID aleatório salvo em localStorage na web.
Retorna: string imediatamente. Requer: nada.
Vincule um teste grátis ao aparelho.
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;
}Conte esta instalação uma única vez nas suas próprias análises.
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');
}Mostre um código de suporte curto que você possa consultar depois, com um botão de copiar.
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>;
}Observações: Permanece o mesmo entre atualizações do app e muda ao desinstalar ou "limpar dados". Não é um ID de hardware. Na web, lança um erro quando localStorage está bloqueado, de propósito - um ID em branco compartilhado misturaria usuários.
appmintNative.isAlarmRinging appmintNative#
isAlarmRinging() — @/lib/appmintNative
Exemplo
Se um dos despertadores do app está tocando agora.
Retorna: boolean, de forma síncrona; sempre false na web. Requer: 🔔 Despertador na etapa Acesso (Etapa 4) ao compilar.
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;
}Mostre os botões Parar e Soneca enquanto um alarme toca:
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>
);
}Verifique o suporte primeiro, para que a página mostre os controles de alarme apenas onde um alarme pode tocar:
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
Exemplo
Informa se o código está rodando dentro do app Android instalado (a ponte do shell window.WebToApk está presente).
Retorna: boolean imediatamente. false em um navegador. Requer: nada.
Mostre um selo "App Android" e um link de download no navegador.
import { isNativeApp } from '@/lib/appmintNative';
export function appBadgeText(): string {
if (isNativeApp()) return 'Android app';
return 'Get the app on Google Play';
}Marque seus eventos de análise com a origem deles.
import { isNativeApp, platform } from '@/lib/appmintNative';
export function analyticsSource(): string {
return isNativeApp() ? `app-${platform()}` : 'browser'; // 'app-android' or 'browser'
}Oculte o banner "Baixe o app" dentro do 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>;
}Observações: platform() dá a mesma resposta como string ('android' no app, 'web' em um navegador). Para decidir se um recurso pode ser mostrado, use can('<capability>') em vez disso: ele sabe qual plataforma tem qual recurso.
appmintNative.keepAlive appmintNative#
import { keepAlive } from '@/lib/appmintNative'
Exemplo
Mantém o app em execução em segundo plano com uma notificação de serviço em primeiro plano (start, update, stop, isRunning, onState, available).
Retorna: start / update → Promise<BgResult> ({ ok, reason?, type?, notificationShown? }; reason: 'unsupported' em um navegador ou no iOS, 'not_enabled' quando a compilação não marcou Manter em execução em segundo plano). stop → Promise<boolean>. isRunning() → boolean. onState(cb) → uma função para cancelar a assinatura; cb recebe o detalhe de appmint:background-service. Requer: Manter em execução em segundo plano na compilação.
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();Pare o serviço e leia por que start foi recusado:
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.');
}
}Mantenha um botão React em sincronia com o serviço e mostre o progresso na notificação:
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>
Exemplo
Mantém a tela ligada enquanto uma receita, uma rota, um QR code ou um timer está sendo exibido e a deixa dormir de novo.
Retorna: Promise<boolean> — true quando o pedido teve efeito, false quando nada pôde manter a tela ligada (ou o navegador recusou). Requer: nada.
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 />;
}Mantenha a tela ligada somente enquanto um cronômetro estiver em execução e avise o usuário quando não for possível.
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]);
}Verifique primeiro o recurso e adquira o bloqueio de novo quando a aba voltar ao primeiro plano (a web o libera quando a página fica oculta).
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);
};
}Observações: No app, usa o plugin KeepAwake (a flag de manter a tela ligada da janela); na web, a Screen Wake Lock API, que os navegadores liberam sozinhos quando a aba fica oculta - chame keepAwake(true) de novo quando a página ficar visível se a tela precisar continuar ligada. Só funciona enquanto o app está em primeiro plano.
appmintNative.Keyboard appmintNative#
Keyboard.hide() / Keyboard.addListener(event, handler): { remove() }
Exemplo
O teclado virtual no formato do plugin Keyboard do Capacitor: feche-o e saiba quando o espaço que ele cobre muda.
Retorna: hide() → Promise<void>. addListener(event, handler) → o formato do Capacitor: uma Promise do handle que também tem remove() por conta própria, então await, .then(h => h.remove()) e .remove() funcionam. event é 'keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide' ou 'keyboardDidHide'; o handler recebe { keyboardHeight }. Requer: nada.
Feche o teclado quando um formulário for enviado ou uma folha abrir sobre o campo:
import { Keyboard } from '@/lib/appmintNative';
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
await Keyboard.hide();
await save();
}Escute (remova o handle ao desmontar):
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(); };
}, []);Combine os dois para rolar o campo em foco até ficar visível quando o teclado abrir e depois fechar o teclado com Enter:
import { Keyboard } from '@/lib/appmintNative';
import { useEffect } from 'react';
export function useKeyboardScroll() {
useEffect(() => {
const h = Keyboard.addListener('keyboardDidShow', () => {
document.activeElement?.scrollIntoView({ block: 'center', behavior: 'smooth' });
});
const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter') void Keyboard.hide(); };
window.addEventListener('keydown', onKey);
return () => { h.remove(); window.removeEventListener('keydown', onKey); };
}, []);
}Observações: keyboardHeight é a parte do teclado que a página ainda precisa liberar, não a altura bruta: no app Android o shell redimensiona a página para o teclado, então é 0 lá enquanto o teclado está aberto (o evento de exibição ainda dispara, e dispara de novo quando o número muda). Os eventos de ocultar só disparam quando o teclado realmente fecha, então podem indicar "o teclado está aberto". Os eventos "Will" e "did" disparam juntos. Só para espaçamento, onKeyboard((px) => …) é mais simples.
appmintNative.launchUrl appmintNative#
launchUrl(): Promise<string>
Exemplo
Fornece o link que iniciou ou retomou o app — um deep link, um atalho de ícone, um toque em widget, uma notificação — para o app abrir a tela certa.
Retorna: Promise<string> - a URL completa, ou '' quando o app foi aberto normalmente. Sempre '' na web, onde a barra de endereço já é a rota. Requer: nada para atalhos e toques em widgets; links externos exigem a opção Deep Linking ao compilar.
Faça o roteamento uma vez, quando o app for montado:
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]);
}Combine com onDeepLink() para links que chegam enquanto o app já está aberto:
import { onDeepLink } from '@/lib/appmintNative';
const off = onDeepLink((url) => console.log('opened with', url));Trate a string vazia como "aberto normalmente" e escolha a tela inicial a partir do link somente quando houver um. Um atalho como appmint-shortcut://<package>/orders tem a rota no caminho.
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); });
}Observaçþes:
- O valor é MANTIDO, não consumido: um componente que monta depois ainda o vê. Um link mais novo o substitui.
- Ele é lido do shell (
WebToApk.getLaunchUrl()); nunca chame isso diretamente em um app criado com IA. Atalhos e toques em widgets chegam comoappmint-shortcut://<package><route>. - Analise com
new URL(u)e navegue até o caminho dele; não carregue a própria URL.
appmintNative.listCallLog appmintNative#
listCallLog(limit = 100, offset = 0, sinceMillis = 0): Promise<CallLogEntry[]>
Exemplo
Lê o histórico de chamadas, das mais recentes para as mais antigas. Somente no app Android.
Retorna: Promise<CallLogEntry[]> — cada { number, name, type, date, duration }; type é incoming, outgoing, missed, voicemail, rejected, blocked ou other, date é em milissegundos de época, duration é em segundos. Vazio quando o Histórico de chamadas está desativado ou a permissão é recusada. Requer: a opção Histórico de chamadas (leitura) na Etapa 4 (Acesso) — a compilação a ativa quando o seu código chama isto — e a permissão do usuário no primeiro uso.
Chamadas perdidas dos últimos 7 dias.
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 total de conversa por contato a partir de chamadas feitas e recebidas, do maior para o menor.
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]);
}Uma tela que explica a lista vazia, já que "sem chamadas" e "permissão recusada" parecem iguais.
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 };
}Observações: limit é limitado a 2000 pelo app. Uma lista vazia não distingue "nenhuma chamada" de "permissão recusada". can('callLog') é 'none' na web. O registro de chamadas é uma permissão restrita do Google Play: você precisa de uma Declaração de Permissões aprovada no Play Console, ou o app é rejeitado.
appmintNative.listContacts appmintNative#
listContacts(limit = 200, offset = 0): Promise<Contact[]>
Exemplo
Lê a agenda de contatos, uma página por vez. Somente no app Android, pela ponte do shell.
Retorna: Promise<Contact[]>. Um array vazio quando não há contatos, quando o acesso é recusado ou quando Contatos não foi ativado na compilação. Requer: o botão Contatos na Etapa 4 (Acesso) - a compilação o ativa quando seu código chama isto - e a permissão do usuário no primeiro uso.
Uma tela de lista de contatos. Mostre-a apenas onde ela puder funcionar. Cada contato é { 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
}
}Pesquise na agenda inteira lendo-a página por página em uma única 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));
}Combine com sendSms para enviar mensagem ao primeiro número de um contato.
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!`);
}Observações: Um resultado vazio não distingue "sem contatos" de "permissão recusada" - mostre uma dica curta ("Permita contatos nas Configurações") quando a lista estiver vazia. Os contatos vêm ordenados de A a Z pelo nome; limit é limitado a 2000. Para uma única pessoa, use pickContact(), que não precisa de permissão. Contatos são dados pessoais: declare isso no seu formulário de Segurança dos dados do Play e na política de privacidade.
appmintNative.listSms appmintNative#
listSms(box: 'inbox' | 'sent' | 'draft' = 'inbox', limit = 100, offset = 0): Promise<SmsMessage[]>
Exemplo
Lê mensagens SMS da caixa de entrada, dos enviados ou dos rascunhos, das mais recentes para as mais antigas. Somente no app Android.
Retorna: Promise<SmsMessage[]> - cada { id, address, body, date, read, box }. Vazio quando o SMS está desativado, a permissão é recusada ou a caixa está vazia. Requer: o botão SMS (ler e enviar) na Etapa 4 (Acesso) - a compilação o ativa quando seu código chama isto - e a permissão do usuário no primeiro uso.
Mostre as últimas 20 mensagens da caixa de entrada.
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
}
}Mensagens não lidas de um remetente, as mais recentes primeiro.
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);
}Leia os enviados e os rascunhos de uma vez e ordene-os juntos por 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));
}Observações: uma lista vazia não distingue "sem mensagens" de "permissão recusada". limit é limitado a 500 pelo app. can('smsList') é 'none' na web. Novas mensagens recebidas não fazem parte deste wrapper; páginas escritas à mão as recebem pelo evento appmint:sms-received (veja listSms para a ponte bruta). SMS é uma permissão restrita do Google Play: você precisa de uma Declaração de Permissões aprovada no Play Console, normalmente concedida apenas a apps de SMS padrão.
appmintNative.liveProgress appmintNative#
import { liveProgress } from '@/lib/appmintNative'
Exemplo
Um cartão de status fixo e silencioso na área de notificações do Android que você atualiza no mesmo lugar - uma entrega, um envio, um cronômetro, um treino. start, update, end.
Retorna: start / update → Promise<boolean> (false quando indisponível, quando as notificações não são permitidas ou quando o usuário desativou a categoria "Status ao vivo"); end → Promise<void>. Requer: o app Android e a permissão de notificação no Android 13+. Na web, can('liveProgress') é 'none' - mostre o mesmo estado dentro do seu app ali.
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
}Um timer em execução: startedAt mostra um relógio contando a partir daquele momento.
await liveProgress.start({ id: 'run', title: 'Morning run', text: '5 km goal', startedAt: Date.now() });
// ...later
await liveProgress.end('run');Trate uma recusa. start responde false; liveProgress.lastError diz o motivo.
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;
}Observações: o mesmo id atualiza um cartão; ids diferentes criam cartões separados (o id padrão é 'live'). O usuário não pode dispensá-lo com um deslize, então sempre chame end. Peça a permissão de notificação primeiro no Android 13+ (Notification.requestPermission()): sem ela start responde false e nada é mostrado.
appmintNative.LocalNotifications appmintNative#
import { LocalNotifications } from '@/lib/appmintNative'
Exemplo
O formato de @capacitor/local-notifications (LocalNotifications.schedule({ notifications })) para código escrito para o plugin do Capacitor. Uma notificação sem schedule é exibida agora (por notify()); uma com schedule: { at } é um lembrete naquele horário, mantido entre reinicializações.
Retorna: Promise<{ notifications: { id }[] }>. Um lembrete que não pode ser definido (um navegador, uma compilação sem Notificações, permissão recusada) rejeita com o motivo — ele nunca é exibido antes da hora no lugar disso. Lembretes repetidos: scheduleAlarm ou a API de lembretes. Requer: Android: Notificações ativadas na etapa Acesso (Etapa 4) ao compilar.
import { LocalNotifications } from '@/lib/appmintNative';
export async function announceSync(count: number) {
await LocalNotifications.schedule({
notifications: [
{ title: 'Sync complete', body: `${count} items updated`, tag: 'sync' },
],
});
}Várias notificações em uma chamada, cada uma substituindo uma anterior que tenha o mesmo 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 precisar saber se foi mostrada, use notify() e reaja à resposta.
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
}Observações: somente imediatas — os campos do Capacitor id, schedule, at, actionTypeId e extra não são compatíveis; cada item é { title, body?, icon?, tag? }. Para uma notificação em um momento posterior, uma página escrita à mão usa WebToApk.scheduleNotificationEx (Android). Para botões use notifyWithActions.
appmintNative.lockOrientation appmintNative#
lockOrientation(to: 'portrait' | 'landscape' | 'any'): Promise<boolean>
Exemplo
Fixa a tela em uma orientação para uma visualização que só faz sentido assim - um tabuleiro de jogo, um gráfico largo, um campo de assinatura. 'any' libera o bloqueio.
Retorna: Promise<boolean> — true quando o bloqueio (ou a liberação) foi aplicado, false quando a plataforma recusou. Nunca rejeita. Requer: nada.
Bloqueie enquanto a tela estiver aberta e libere ao sair:
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>;
}Verifique a resposta quando a tela depender dela:
import { lockOrientation, toast } from '@/lib/appmintNative';
const ok = await lockOrientation('portrait');
if (!ok) toast('Turn your phone upright for the best view');Libere o bloqueio quando a tela sair, como um hook compartilhado por várias telas.
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 screenObservaçþes:
- No app, o plugin ScreenOrientation do Capacitor aplica o bloqueio.
- Na web usa
screen.orientation.lock(). A maioria dos navegadores recusa fora da tela cheia e os de desktop sempre recusam, então esperefalselá. - Sempre dê ao usuário uma saída de uma tela bloqueada.
appmintNative.longPress appmintNative#
longPress(el: HTMLElement, handler: (e: PointerEvent) => void, opts?: { ms?: number; haptics?: boolean }): () => void
Exemplo
Avisa você quando o usuário mantém o dedo sobre um elemento - para o menu de contexto de uma linha, reações ou ações de cartão.
Retorna: uma função para cancelar a assinatura - chame-a ao desmontar. Requer: nada.
Opções: ms - tempo de pressão, padrão 450; haptics - vibração média quando dispara, padrão true.
Abra uma folha de ações ao pressionar e segurar, com um botão visível para o mesmo 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>
);
}Um toque mais longo, sem o háptico:
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 });Combine com haptic para uma confirmação personalizada e faça a limpeza para cada linha da 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>;
}Observaçþes:
- Dispara uma vez por toque longo e engole o clique que viria depois, então um toque longo não conta também como "toque".
- Mover mais de 8 px cancela o gesto, então rolar a tela nunca o dispara. Ele também bloqueia o menu de toque longo do próprio navegador nesse elemento.
- Um toque longo é invisível. Mantenha sempre um botão visível para a mesma ação.
appmintNative.markPushRead appmintNative#
import { markPushRead } from '@/lib/appmintNative'
Exemplo
Marca uma mensagem da caixa de entrada do AppMint Push como lida, ou todas as mensagens quando chamado sem argumento.
Retorna: boolean de forma síncrona - true se algo mudou. Sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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();
}Conte as mensagens não lidas depois de marcar uma, para um selo no seu próprio cabeçalho.
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 };
}Marque tudo como lido quando o usuário abrir a tela da caixa de entrada e atualize quando chegar uma nova mensagem.
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;
}Observações: o estado de leitura fica apenas neste celular; a notificação na bandeja não é removida.
appmintNative.mediaSession appmintNative#
mediaSession — @/lib/appmintNative
Exemplo
Controles de reprodução na tela de bloqueio e na notificação para um app que toca áudio ou vídeo: publique o que está tocando, receba os botões Play / Pause / Próximo / Anterior / Parar e envie um vídeo para uma TV.
Retorna: setMetadata, setPlaybackState, clear → void. onAction(handler) → uma função para cancelar a inscrição. cast(url) → Promise<boolean> (false quando nada pôde ser oferecido). Requer: Notificação de mídia na Etapa 4 (Acesso) ao compilar; compilações com IA o ativam para você quando o código 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} />;
}Envie o vídeo atual para uma 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>;
}Limpe o cartão da tela de bloqueio quando a reprodução terminar e não mexa nele quando a plataforma não tem controles de mídia.
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);
}Observaçþes:
- No Android, os botões chegam pelo
navigator.mediaSessiondo app, instalado quando a página termina de carregar. UmonActionregistrado antes (por exemplo, em um efeito de montagem) é perdido, então registre-o na primeira reprodução. - A ação
seek(compositionMs) é entregue somente na web; a notificação do Android não tem avanço por posição. artworkUrlprecisa ser um endereçohttps://completo na internet. No Android,cast(url)abre o seletor "Transmitir para..." (um repasse; resolvetrueassim que o seletor é solicitado). Na web usa a Remote Playback API.
appmintNative.Motion appmintNative#
Motion.addListener('accel' | 'orientation', cb): Promise<{ remove() }>
Exemplo
O formato @capacitor/motion sobre os eventos padrão devicemotion e deviceorientation: movimento e inclinação do celular. O mesmo código funciona no app e em um navegador.
Retorna: Promise<{ remove(): Promise<void> }> — chame remove() para parar. 'accel' chama de volta com { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }; 'orientation' com { alpha, beta, gamma } (graus). Requer: nada.
Agite para atualizar.
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;
}Incline uma bolha de nível com o evento de orientação.
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 de hook com limpeza automática para um componente React que lê o movimento do celular.
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 };
}Observações: alguns navegadores pedem acesso ao movimento primeiro, então inicie a partir de um toque neles; se o usuário recusar, o handle é retornado, mas nenhum evento chega. can('motion') é 'none' onde o navegador não tem DeviceMotionEvent. Valores de sensor ausentes chegam como 0. Sempre use remove() quando a tela fechar — eventos de movimento disparam várias vezes por segundo.
appmintNative.musicVolume appmintNative#
musicVolume() — @/lib/appmintNative
Exemplo
Lê o nível da música de fundo em loop do app, de 0 a 100.
Retorna: number, de forma síncrona: 0-100, ou -1 quando não há música (na web, ou em um app compilado sem arquivo de música). Requer: um arquivo escolhido em Música de fundo na Etapa 3 (Integrar) ao compilar.
Desenhe um controle deslizante de música apenas quando houver música:
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>
);
}Mantenha o nível em sincronia com um botão "Silenciar música", lembrando o volume para voltar a ele.
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>;
}Diminua o volume da música enquanto um vídeo toca e depois restaure.
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));
}Observações: Somente no app Android; a web responde -1. Um nível definido com setMusicVolume é mantido para a próxima abertura; até a página definir um, é o nível da compilação.
appmintNative.Network appmintNative#
Network.getStatus(): Promise<{ connected, connectionType }> / Network.addListener(event, handler): { remove() }
Exemplo
Estado online/offline no formato do plugin Network do Capacitor - o mesmo código de networkStatus() / onNetworkChange().
Retorna: getStatus() → Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) → o formato do Capacitor: uma Promise do handle que também tem remove() por conta própria; o handler recebe o mesmo formato a cada mudança. Requer: nada.
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;
}Leia o tipo atual uma vez e mostre-o em um chip de status.
import { Network } from '@/lib/appmintNative';
import { useEffect, useState } from 'react';
export function ConnectionChip() {
const [label, setLabel] = useState('Checking...');
useEffect(() => {
void Network.getStatus().then((s) =>
setLabel(s.connected ? (s.connectionType === 'cellular' ? 'Mobile data' : 'Wi-Fi') : 'Offline'));
}, []);
return <span className="chip">{label}</span>;
}Segure um download grande em dados móveis e trate também o caso sem conexão.
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();
}Observações: O nome do evento é ignorado - todo listener recebe as mudanças de conectividade. connectionType é 'wifi', 'cellular', 'none' ou 'unknown': no app, getStatus() e o listener usam o plugin Network; na web, navigator.onLine / navigator.connection.type. connected significa que o celular tem uma rede, não que o seu servidor responde.
appmintNative.networkStatus appmintNative#
networkStatus(): { connected: boolean; type: string }
Exemplo
Diz, agora mesmo, se o dispositivo está online.
Retorna: { connected, type }, de forma síncrona. type é 'wifi', 'cellular', 'none' (offline) ou 'unknown'. Requer: nada.
Leve o bastante para chamar durante a renderização:
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>
);
}Leia o valor inicial e depois acompanhe as mudanças:
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;
}Escolha um tamanho de imagem mais leve em dados móveis, tratando também o 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'}`;
}Observaçþes:
- No app, é a resposta do plugin Network (mantida atualizada em segundo plano, então a primeira chamada ainda pode ler o valor da própria WebView). Na web, é
navigator.onLinemaisnavigator.connection.typequando o navegador informa um, senão'unknown'- uma classe de velocidade como'4g'nunca é informada como o tipo. - "Conectado" significa que o dispositivo tem uma rede, não que o seu servidor responde. Trate as requisições com falha de qualquer forma.
appmintNative.nfc appmintNative#
import { nfc } from '@/lib/appmintNative'
Exemplo
Funções auxiliares de uma chamada sobre o Web NFC (NDEFReader): verificar suporte, ler tags, gravar uma tag de texto. O app Android usa seu próprio NDEFReader; na web é o Web NFC do próprio Chrome (Chrome no Android).
Retorna: isSupported(): Promise<boolean>; scan(onTag): Promise<() => Promise<void>> (a função de parada), e onTag recebe Array<{recordType, text, mediaType?}> por tag - os nomes de tipo do Web NFC ('text', 'url', 'mime', …) e o texto decodificado em todas as plataformas; write(text): Promise<boolean> (true quando uma tag foi gravada, false em qualquer falha). Requer: no Android, ative Suporte a Web NFC na Etapa 4 (Acesso) ao compilar. scan() lança um erro quando não há NDEFReader - verifique can('nfc') antes.
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>
);
}Grave um link em uma tag e diga ao usuário por que falhou, quando falhar.
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.');
}Leia uma tag e depois pare de escanear.
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);
});
}Observações: No Android, isSupported() é true sempre que a compilação tem NFC - não diz se o NFC está ligado (window.AndroidNFC.startScan() diz). Depois da função de parar, onTag nunca mais é chamado. No Android, o próprio app continua capturando tags enquanto está em primeiro plano (para que um toque não abra outro app), apenas deixa de repassá-las a você. O text de um registro de texto não tem prefixo de idioma; o text de um registro de URL é a URL completa. write() espera até que uma tag seja tocada. Para gravar registros de URL ou mime e para travar uma tag, use NDEFReader diretamente.
appmintNative.notify appmintNative#
import { notify } from '@/lib/appmintNative'
Exemplo
Mostra uma notificação agora mesmo a partir de um app criado com IA, no app Android e na web.
Retorna: Promise<boolean> — true quando publicada, false quando o usuário recusou a permissão de notificação, as notificações estão desativadas para o app ou não estão disponíveis aqui. Ela pede a permissão sozinha na primeira vez (Android 13+ e web). Requer: Android: Notificações ativadas na etapa Acesso (Etapa 4) ao compilar.
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.');
}Peça em um bom momento: o primeiro notify() mostra a caixa de permissão. Para pedir a partir do seu próprio botão "Ativar alertas" - antes de qualquer coisa ser publicada - use a API padrão, que o app também suporta:
async function enableAlerts() {
if (typeof Notification === 'undefined') return false;
const perm = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
return perm === 'granted';
}Abra a tela certa quando o usuário tocar na notificação, combinando notify com 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;
}Observações: O Android usa a ponte de notificações do shell (icon vira o ícone grande; use uma imagem https:// ou data:); a web usa a Notification API. Depois que o usuário recusa de vez, notify() resolve false sem perguntar de novo - leve-o às configurações do celular. Para botões use notifyWithActions; para um cartão fixo de progresso use liveProgress.
appmintNative.notifyWithActions appmintNative#
import { notifyWithActions } from '@/lib/appmintNative'
Exemplo
Mostra uma notificação com até 3 botões ("Concluir", "Adiar"). O toque volta por meio de onNotificationAction.
Retorna: Promise<boolean> — true quando publicada, false quando a permissão de notificação foi recusada (ela é pedida na primeira vez, como em notify()). Requer: Android: Notificações ativadas na etapa Acesso (Etapa 4) ao compilar e a permissão do usuário no Android 13+. Na 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);
});Um timer terminou: um botão adiciona cinco minutos, o outro o encerra.
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);
});Trate uma permissão recusada depois da chamada e faça a limpeza em um 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>;
}Observações: cada botão mostra o seu title; o seu id é o que volta como e.actionId. A opção input (caixa de resposta inline) não é compatível neste app — é ignorada, o botão não tem caixa de resposta e e.text é sempre ''. Um toque só chega à página enquanto o app está em execução. Na web o wrapper publicaria uma notificação simples sem botões, por isso o exemplo verifica can() primeiro.
appmintNative.offlineBanner appmintNative#
offlineBanner(opts?: { message?: string; onlineMessage?: string }): () => void
Exemplo
Monta uma faixa pronta que desliza sob a barra de status quando a conexão cai e diz brevemente "De volta online" quando ela volta.
Retorna: uma função de cancelamento que remove o banner e para de escutar. Requer: nada.
Monte-o uma vez na inicialização do app:
import { useEffect } from 'react';
import { offlineBanner } from '@/lib/appmintNative';
export function App() {
useEffect(() => offlineBanner(), []); // cleanup removes it
return <main>{/* … */}</main>;
}Suas próprias palavras (por exemplo, no idioma do app):
import { offlineBanner } from '@/lib/appmintNative';
const off = offlineBanner({ message: 'Sin conexión', onlineMessage: 'Conectado de nuevo' });Monte o banner apenas para a área logada do app e remova-o ao sair da conta.
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]);
}Observaçþes:
- Padrões: "Você está offline" e "De volta online". A mensagem de online some após cerca de 1,8 segundo; a de offline fica até a conexão voltar.
- Se o aparelho já estiver offline quando for montado, o banner aparece imediatamente.
- Construído sobre
onNetworkChange(), então é real em todas as superfícies. As cores vêm dos tokens do app (--ink/--bgoffline,--accent/--accent-inkonline). Temrole="status"para leitores de tela. - Monte-o uma vez. Duas chamadas desenham dois banners.
appmintNative.onAlarm appmintNative#
onAlarm(cb) — @/lib/appmintNative
Exemplo
Escuta os resultados de alarmes: o app aberto por um alarme (action: 'open', com ringing) e como um toque terminou - dismiss, snooze, missed (tocou até o fim, ou foi substituído por outro alarme) ou error. Os resultados de quando o app estava fechado são entregues quando a página carrega de novo.
Retorna: uma função que para de ouvir. O callback recebe { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. Requer: 🔔 Despertador na etapa Acesso (Etapa 4) ao compilar.
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;
}Defina um alarme, respondendo alarmSupport() primeiro.
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.';
}Pare ou adie o alarme que está tocando a partir da sua própria tela e reaja ao resultado.
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>
);
}Observações: lembretes comuns que abrem o app também chegam aqui com action: 'open' e ringing: false.
appmintNative.onBackGesture appmintNative#
onBackGesture(handler: () => boolean): () => void
Exemplo
Deixa uma janela, modal, lightbox ou etapa de assistente abertos responderem primeiro ao gesto de VOLTAR — o deslize da borda / botão voltar do Android no app, o botão voltar do navegador na web.
Retorna: uma função de cancelamento — chame-a quando a tela fechar ou for desmontada. Seu handler retorna true para consumir o toque (você fechou algo) ou false para deixá-lo passar. Requer: nada.
Registre somente enquanto a folha estiver aberta:
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>}
</>
);
}Volte etapa por etapa em um assistente e deixe o voltar da última etapa sair 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]);
}Feche uma lightbox com o gesto de voltar, mantendo o registro atrelado à imagem estar aberta.
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;
}Observaçþes:
- Os manipuladores formam uma pilha: o registrado por último (a folha no topo) é consultado primeiro. Um manipulador que lança exceção não consome nada.
- No Android passa pelo handler de voltar do shell. Sem ele, deslizar para voltar com uma janela aberta fecha o app inteiro.
- Na web, ele adiciona uma entrada de histórico enquanto houver algum handler registrado e a remove quando o último cancelar a assinatura.
- Ainda assim, desenhe um controle visível de voltar / fechar: muitos usuários nunca usam o gesto de voltar.
- O React Router já cuida do voltar normal de página; use isto só para estados que o roteador não conhece. Sempre cancele a assinatura, ou uma folha fechada continua consumindo os toques em voltar.
appmintNative.onDeepLink appmintNative#
onDeepLink(cb: (url: string) => void): () => void
Exemplo
Chama você quando um link abre o app enquanto ele já está em execução - um deep link, um atalho do ícone, um toque em widget.
Retorna: uma função de cancelamento — chame-a ao desmontar. Requer: nada para atalhos e toques em widgets; links externos precisam da opção Deep Linking ao compilar.
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;
}Uma página escrita à mão (sem React) pode escutar diretamente o mesmo evento do shell:
window.addEventListener('appmint:deep-link', function (e) {
var url = e.detail && e.detail.url;
if (url) console.log('deep link', url);
});Abra um produto específico quando um widget ou atalho for tocado, ignorando links que não são nossos.
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;
}Observaçþes:
- Android: escuta o evento
appmint:deep-linkdo shell (detail.url); o shell também chamawindow.onAppMintDeepLink(detail)quando uma página o define. Depois que um link chega,launchUrl()também o retorna. - Web: nunca dispara (o callback não é chamado) e o cancelamento da assinatura não faz nada.
appmintNative.onInAppMessage appmintNative#
import { onInAppMessage } from '@/lib/appmintNative'
Exemplo
Avisa você a cada mensagem no app (as campanhas do criador) que é exibida, clicada, dispensada ou respondida nesta instalação. Retorna a função para cancelar a assinatura.
Retorna: () => void. O callback recebe {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. Nunca dispara na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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]);
}Desenhe as mensagens você mesmo (com setInAppPresentation('none'), presented é false e message traz o 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) };
}Reaja a um botão que o usuário tocou no desenho do próprio app:
import { onInAppMessage } from '@/lib/appmintNative';
onInAppMessage((e) => {
if (e.type === 'clicked' && e.link === 'appmint://notifications/enable') refreshNotificationBadge();
});Observações: button é -1 para um toque na própria mensagem; caso contrário, é o índice do botão.
Respostas (type: 'answered'). Uma mensagem com uma linha de estrelas (pesquisa de estrelas, Avalie-nos) ou fichas de nota de 0 a 10 (nota NPS) informa a resposta uma vez, quando fecha: question é 'stars' (valor 1-5) ou 'nps' (valor 0-10), e template indica o modelo da galeria de onde veio. A central de envio mostra a média ou a nota NPS; seu código pode dar sequência:
import { onInAppMessage } from '@/lib/appmintNative';
onInAppMessage((e) => {
if (e.type === 'answered' && e.question === 'nps' && (e.value ?? 10) <= 6) openFeedbackSheet();
});Mantenha seu próprio registro das avaliações por estrelas:
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 }) });
});Agradeça a quem avaliou bem, em um 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
Exemplo
Informa quantos pixels da página o teclado na tela cobre, a cada mudança, para que uma barra inferior possa ficar acima dele.
Retorna: uma função para cancelar a inscrição - chame-a ao desmontar. O callback recebe um número em pixels CSS: o preenchimento inferior extra de que você precisa, 0 quando o teclado está fechado. Requer: nada.
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>
);
}Mantenha uma lista de chat rolada até a mensagem mais recente enquanto o teclado abre:
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>;
}Um hook reutilizável, com um espaço de área segura que desaparece enquanto o teclado está aberto:
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>Observaçþes:
- No app ele escuta o plugin Keyboard do Capacitor e informa somente a parte do teclado que o layout ainda não absorveu. O shell geralmente redimensiona a página para o teclado, então o valor costuma ser
0lá — isso está correto; não some você mesmo a altura bruta do teclado. - Em um app em tela cheia ou quiosque (página não redimensionada), informa a altura total do teclado.
- Na web usa
visualViewport, chama o callback uma vez de imediato e trata menos de 80 px como a interface do navegador (informado como0). - No app a primeira chamada vem quando o teclado abre pela primeira vez, então comece seu estado em
0.
appmintNative.onNetworkChange appmintNative#
onNetworkChange(cb: (status: { connected: boolean; type: string }) => void): () => void
Exemplo
Chama você sempre que a conexão cai ou volta — para um banner offline ou para repetir gravações em fila.
Retorna: uma função para cancelar a assinatura - chame-a ao desmontar. Requer: nada.
Tente de novo o trabalho enfileirado quando o dispositivo voltar a ficar 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]);
}Mostre o estado atual de imediato e depois acompanhe as mudanças (o callback não é chamado ao se inscrever):
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;
}Segure downloads grandes em dados móveis e trate 'none' como 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]);
}Observaçþes:
- NÃO chama você com o estado atual quando você se inscreve. Leia
networkStatus()para obter o valor inicial. - No app, ouve o plugin Network do Capacitor (
networkStatusChange);typeé'wifi','cellular','none'ou'unknown'. - Na web, ouve os eventos
online/offlinee informanetworkStatus(). - Para um banner pronto, use
offlineBanner(), que é construído sobre isto.
appmintNative.onNotificationAction appmintNative#
import { onNotificationAction } from '@/lib/appmintNative'
Exemplo
Escuta os toques nos botões de notificação (de notifyWithActions).
Retorna: uma função que para de ouvir. O callback recebe { actionId, tag } - o ID do botão e a tag da notificação. O app Android não envia text de resposta (resposta em linha não é suportada), então não dependa de e.text. Requer: o mesmo que 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 bruto em uma página escrita à mão (app Android):
window.addEventListener('appmint:notification-action', function (e) {
// e.detail = { actionId: 'Mute', tag: 'chat-7' }
console.log(e.detail.actionId, e.detail.tag);
});Publique uma notificação com botões apenas onde a compilação os suporta e trate o toque pela tag da notificação:
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]);
}Observações: no app Android este é o evento appmint:notification-action (também window.onAppMintNotificationAction(detail)); na web não faz nada. Um toque só é entregue enquanto o app está em execução - tocar em um botão não abre o app.
appmintNative.onNotificationClick appmintNative#
import { onNotificationClick } from '@/lib/appmintNative'
Exemplo
Executa um callback quando o usuário toca em uma notificação publicada pelo app - inclusive o toque que abriu o app a frio - para que o app possa abrir a tela certa.
Retorna: a função de cancelamento. O callback recebe { tag, title, body, data } — data é o que você passou para notify({ …, data }). Requer: notificações, como notify(). Evento por trás: 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 } });
}Abra a tela certa após o toque, mesmo quando ele iniciou o app do zero, e ignore notificações que não carregam dados:
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]);
}Envie uma notificação somente onde existem notificações, com dados para o toque (o data precisa de um 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');
}Observações: O toque viaja pela tela de abertura e pela tela de PIN até a página. data deve ser JSON (abaixo de 16 KB) e precisa de um tag.
appmintNative.onPullToRefresh appmintNative#
onPullToRefresh(el: HTMLElement, onRefresh: () => void | Promise<void>, opts?: { threshold?: number }): () => void
Exemplo
Adiciona o gesto de puxar para baixo para atualizar a uma lista rolável: puxe para baixo no topo, solte, e seus dados são recarregados.
Retorna: uma função para cancelar a assinatura - chame-a ao desmontar. Requer: nada.
el é o elemento que rola (ou document.documentElement quando a página inteira rola). O indicador de carregamento permanece até a promise de onRefresh ser concluída:
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>;
}Uma página que rola inteira, com um puxão mais longo:
import { onPullToRefresh } from '@/lib/appmintNative';
declare function reloadFeed(): Promise<void>;
const off = onPullToRefresh(document.documentElement, reloadFeed, { threshold: 96 });Atualize uma lista por meio de um hook de dados, com um toque háptico quando os dados chegarem:
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">
}Observaçþes:
- Só começa quando a lista está rolada até o topo. O padrão de
thresholdé 72 px de puxada. - Desenha um indicador redondo nas cores do app (
--surface,--accent), dispara um háptico médio no ponto de confirmação e ignora novos puxões enquanto uma atualização está em andamento. - Atualiza seus DADOS, não a página. O puxar para atualizar do próprio shell que recarrega a página (Ativar puxar para atualizar na Etapa 3 (Integrar)) vem desativado por padrão - mantenha-o desativado ao usar isto, para que um puxão não recarregue também a página.
- Baseado em Pointer Events, então é igual em todas as plataformas. Define
overscroll-behavior-y: containemele restaura ao cancelar a inscrição.
appmintNative.onVolumeChange appmintNative#
onVolumeChange(cb) — @/lib/appmintNative
Exemplo
Chama cb sempre que um dos níveis de volume do celular muda — teclas físicas, controle deslizante do sistema, outro app.
Retorna: uma função que para de escutar (a última a parar desliga o monitoramento nativo). O callback recebe { stream, current, max, percent, muted? } para cada canal que mudou. Não faz nada na web. Requer: nada.
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>;
}Avise quando o volume de mídia está no mínimo enquanto um vídeo toca e pare de escutar ao desmontar:
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]);
}Espelhe o nível do celular em um controle deslizante e grave de volta a mudança do usuário (a web não tem volume, então getVolume é null ali):
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>
Exemplo
Abre a tela de configurações de notificação do próprio app (app Android).
Retorna: Promise<boolean> — verdadeiro quando a tela abriu; falso na web e onde essa tela não existe. Verifique can('notificationSettings') antes de desenhar o controle. Requer: nada.
import { can, openNotificationSettings } from '@/lib/appmintNative';
export function NotificationSettingsRow(row: HTMLElement) {
row.hidden = can('notificationSettings') === 'none';
row.addEventListener('click', () => { void openNotificationSettings(); });
}Observações: para um "Ativar notificações" de um toque, use enableNotifications(), que abre esta tela apenas quando a caixa de permissão não pode mais ser exibida.
appmintNative.passkeys appmintNative#
passkeys — available / create / get / origin / assetLinks
Exemplo
Login e cadastro com passkey usando as opções JSON que seu servidor de passkeys gera. No app Android usa o gerenciador de passkeys do Android; na web usa navigator.credentials.
Retorna: available() → 'native' | 'web' | 'none'; create(optionsJSON) → Promise do RegistrationResponseJSON; get(optionsJSON) → Promise do AuthenticationResponseJSON (envie qualquer um de volta ao seu servidor com POST). Eles rejeitam com um DOMException (NotAllowedError, InvalidStateError, SecurityError, NotSupportedError). origin() → string, assetLinks() → array (somente no app; '' / [] na web). Requer: um servidor de passkeys; no app, também Android 9+ com Google Play services e o do seu domínio 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),
});
}Valores de configuração únicos, do app instalado pelo 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 originsOfereça o botão de passkey somente onde ele pode funcionar e avise quando não puder:
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>;
}Observações: Na web, exige uma página segura (https://). Uma página incluída no app não tem domínio: as opções do seu servidor devem definir rp.id / rpId como seu domínio. O preenchimento automático de passkeys (mediation: 'conditional') não está disponível no app - comece a partir de um botão.
appmintNative.paste appmintNative#
paste(): Promise<string>
Exemplo
Lê o texto da área de transferência do sistema.
Retorna: Promise<string> - o texto da área de transferência, ou '' quando ela está vazia. REJEITA quando este navegador não consegue ler a área de transferência de forma alguma, ou quando o usuário nega a permissão. Requer: nada.
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>
);
}A forma no estilo Capacitor:
import { Clipboard } from '@/lib/appmintNative';
const { value } = await Clipboard.read();Cole em uma caixa de texto com uma mensagem para cada resultado e combine com 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');
}Observações: no app o plugin Clipboard lê o texto; uma área de transferência vazia retorna '', não um erro. Em um navegador é navigator.clipboard.readText() (somente Chromium), que pede permissão ao usuário — chame a partir de um toque.
appmintNative.pickContact appmintNative#
pickContact(): Promise<Contact | null>
Exemplo
Abre o seletor de contatos do sistema para o usuário escolher uma pessoa: o seletor do Android no app, a Contact Picker API no Chrome para Android.
Retorna: Promise<Contact | null> - null quando o usuário cancela ou o acesso é recusado. Requer: nada (o seletor do sistema não precisa de permissão).
Escolha uma pessoa e preencha um formulário. O contato tem o mesmo formato no app e na 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
}
}Um botão "Escolher um contato" que mostra a escolha e uma caixa de número simples onde o seletor não existe:
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>
);
}Entregue o número escolhido ao app de mensagens, com um aviso quando o contato não tiver nenhum:
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)}`;
}Observações: no app só volta o número tocado, nunca e-mails, e photoUri é um endereço content:// da foto do contato. can('contactPick') é 'none' em navegadores sem a Contact Picker API (desktop, Safari) — não exiba o botão neles.
appmintNative.pickFile appmintNative#
pickFile(accept?) from '@/lib/appmintNative'
Exemplo
Permite ao usuário escolher um arquivo e lê-lo como texto. No app, abre o seletor de documentos do Android; na web, abre um campo de arquivo normal.
Retorna: Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }, ou null apenas quando o usuário cancela. Rejeita com um Error cujo code é 'no-picker' (o celular não tem app seletor de documentos), 'disabled' ou 'failed' quando nenhum seletor pôde abrir. text() lê o arquivo inteiro e rejeita com o código da ponte (por exemplo, 'not-found') quando não consegue. Requer: nada. Com Acesso nativo a pastas (SAF) marcado na Etapa 4 (Acesso), o app usa o seletor de documentos do Android; sem isso, usa o seletor de arquivos da própria página.
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)}`);
}
}Importe um backup JSON e trate cada forma de falha de pickFile:
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');
}
}Escolha um arquivo de texto e coloque-o em um editor, ao lado dos detalhes do arquivo:
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>
);
}Observações: No app, text() lê o arquivo em pedaços de 4 MB até o fim e o decodifica como UTF-8, então arquivos grandes são lidos por inteiro - mantenha seu próprio limite de tamanho para o que a tela consegue lidar. Um seletor que não consegue abrir rejeita, então envolva pickFile() também em try/catch. Na web, text() é o File.text() do próprio navegador. Para arquivos binários ou para gravar de volta, páginas escritas à mão usam WebToApkFS.
appmintNative.pip appmintNative#
pip.enter(video?) / pip.exit(video?)
Exemplo
Mantém um vídeo tocando em uma pequena janela flutuante enquanto o usuário faz outra coisa.
Retorna: enter() retorna Promise<boolean> - true quando a janela flutuante abriu. exit() retorna Promise<void>. Nenhum rejeita. Requer: no app Android, a opção Picture-in-Picture (PiP) da Etapa 3 (Integrar) na compilação - ela é ativada automaticamente quando seu código chama pip.enter(.
Um botão "minimizar" em um 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>
);
}Saia dele quando o player fechar (web):
import { pip } from '@/lib/appmintNative';
declare const video: HTMLVideoElement;
await pip.exit(video);Entre em picture-in-picture automaticamente quando o usuário sair de um vídeo em reprodução, somente onde há suporte:
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]);
}Observaçþes:
- Android (8+): o app INTEIRO encolhe para a janela flutuante, e o argumento
videoé ignorado. Retornafalseem Android mais antigo ou em uma compilação sem a opção PiP. O usuário expande a janela para voltar;exit()não faz nada ali. - Na web: o picture-in-picture do próprio elemento
<video>. O vídeo precisa estar carregado (em geral, tocando); uma chamada semvideoretornafalse. - Chame a partir de um toque.
appmintNative.platform appmintNative#
platform(): 'android' | 'ios' | 'web'
Exemplo
Informa em qual plataforma o código roda: o app Android instalado ou um navegador web.
Retorna: uma string, imediatamente: 'android' dentro do app, 'web' em um navegador. Pergunta primeiro ao Capacitor (Capacitor.getPlatform()), depois verifica a ponte do shell Android, senão 'web'. O tipo também lista 'ios', mas um app AppMint nunca o recebe - o AppMint gera apenas apps Android. Requer: nada.
Escolha o link certo e copie.
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';Mostre controles exclusivos do 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>;
}Marque eventos de análise com a plataforma.
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 }),
});
}Observações: platform() nunca responde 'web' dentro do app instalado. Não o use para decidir se um recurso existe - muitos recursos também dependem de opções da compilação; can('<capability>') responde a isso.
appmintNative.privacyScreen appmintNative#
privacyScreen — enable() / disable() / isEnabled()
Exemplo
Bloqueia capturas de tela, gravação de tela e a prévia do seletor de apps enquanto uma tela com segredos está aberta.
Retorna: enable() → Promise<boolean> (false onde nada pode bloqueá-la); disable() → Promise<void>; isEnabled() → Promise<boolean>. Requer: nada. No app Android usa o bloqueio de capturas de tela do shell (capturas de tela, gravação de tela, transmissão e a miniatura de recentes).
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
}, []);
}Diga ao usuário o que está protegido - somente quando realmente estiver:
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' : '';
}Ative a proteção em uma tela sensível e avise o usuário se não foi possível aplicá-la:
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();
}Observações: No app, o bloqueio pertence à página atual e é liberado quando o app navega para outra rota, então ative-o de novo quando essa tela aparecer novamente. Na web, can('privacyScreen') é 'none' e enable() retorna false - não afirme que há proteção ali. Nada impede uma câmera apontada para a tela.
appmintNative.pushInbox appmintNative#
import { pushInbox } from '@/lib/appmintNative'
Exemplo
Retorna as mensagens do AppMint Push que esta instalação recebeu (mais recentes primeiro, até 50), para uma tela de notificações dentro do app.
Retorna: PushMessage[] de forma síncrona: { id, title, body, imageUrl, link, receivedAt, read }. Um array vazio na web e quando o AppMint Push está desativado. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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 };
}Mostre uma contagem de não lidas no selo que acompanha as novas chegadas:
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;
}Renderize a lista, com um estado vazio para a web e para compilações sem AppMint Push, e um botão "Marcar tudo como lido":
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>
);
}Observações: a caixa de entrada é a verdade salva - leia-a na montagem; o evento appmint:push só dispara enquanto o app está na tela. As mensagens dispensadas ficam de fora.
appmintNative.readBarcodeFrom appmintNative#
readBarcodeFrom(source: Blob | CanvasImageSource, formats?: BarcodeFormat[]): Promise<{ value, format }[]>
Exemplo
Lê códigos de barras e QR codes de uma imagem que a página já tem - um arquivo de foto, um Blob, um <img>, um <canvas> ou um quadro de vídeo. Nenhuma câmera é aberta.
Retorna: Promise<{ value: string; format: string }[]> — todos os códigos encontrados, na ordem do detector; um array vazio quando não há nenhum, quando a imagem não pode ser lida ou quando a leitura de código de barras não está disponível. Nunca rejeita. Requer: nada.
Leia um código de uma foto que o usuário escolhe:
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>
);
}A partir de uma imagem que já está na página:
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);Leia um código de um quadro de canvas, verificando antes o que o dispositivo suporta:
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;
}Observaçþes:
- Usa o
BarcodeDetectordo mecanismo do navegador (presente na WebView do app Android; ausente no Safari e em muitos navegadores de desktop - nesse caso retorna[]). - Um
<input type="file" accept="image/*">simples abre o seletor de fotos do Android e não precisa de permissão de armazenamento. - Restrinja
formatsquando você os conhecer: fica mais rápido e lê errado com menos frequência.barcodeFormats()lista o que o dispositivo suporta.
appmintNative.readOtp appmintNative#
import { readOtp } from '@/lib/appmintNative'
Exemplo
Aguarda o código SMS de uso único para este app e o retorna - a folha de consentimento de SMS do Google no app Android, WebOTP no Chrome na web.
Retorna: Promise<string | null> - o código, ou null quando o usuário recusou, passaram cinco minutos ou signal abortou. Lança um erro quando can('otp') é 'none'. Requer: nada para ativar. O SMS deve terminar com @<host> #<code> (no app Android o 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" />
);
}Trate a resposta "sem código" e ofereça um reenvio quando a espera terminar sem um:
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>
);
}Capture o erro lançado quando você não puder verificar can('otp') antes:
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
}
}Observações: é um acelerador - mantenha sempre o campo digitável.
appmintNative.recognizeText appmintNative#
import { recognizeText } from '@/lib/appmintNative'
Exemplo
Lê o texto em uma imagem no dispositivo (can('ocr')): o reconhecedor ML Kit do app Android, ou o TextDetector do Chrome onde estiver ativado.
Retorna: Promise de { text, blocks: [{ text, box, lines: [{ text, box }] }] }, com caixas em pixels da imagem. Rejeita com code 'unavailable' onde can('ocr') é 'none', ou com uma DOMException quando o reconhecimento falha. Requer: nada, sem permissão.
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>
);
}Trate explicitamente os casos de "sem reconhecedor" e de falha:
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
}
}Leia um alfabeto diferente do latino e liste cada linha com a sua posição:
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 }));
}Observações: Apenas escritas latinas. A primeira chamada em um celular novo pode levar alguns segundos enquanto o Play services baixa o modelo - mostre o progresso.
appmintNative.remoteConfig appmintNative#
remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()
Exemplo
Desenha a faixa de aviso e a tela de manutenção que o criador publica pelo Remote Update — para a página quando ela roda em um navegador. No app Android o shell já faz isso sozinho, então estas chamadas não fazem nada lá.
Retorna: start(manifestUrl?) → Promise<void> (busca + desenha; nunca rejeita); fetch(url) → Promise<RemoteConfig | null> (nunca rejeita; null = nada se aplica); apply(cfg) → uma função de limpeza; manifestUrl() → string (o content de <meta name="appmint-remote-manifest">, '' quando não há nenhum); handledHere() → boolean; dismissed(id) → boolean; dismiss(id) → void. Requer: nada no app - a Atualização Remota é nativa lá.
Seguro chamar na inicialização; no app, retorna na hora:
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>;
}Ou leia os valores e desenhe sua própria interface (somente 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)Mostre você mesmo o texto de manutenção na web e deixe o app com a própria tela:
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;
}Observaçþes:
RemoteConfig={ version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }.handledHere()éfalseno app Android: o shell lê o manifesto de forma nativa e desenha o aviso por conta própria, então você nunca vê dois banners.start()sem argumento usamanifestUrl(). Uma compilação do AppMint não adiciona essa meta tag, então sem argumento (ou sem o seu próprio<meta name="appmint-remote-manifest" content="…">) nada é buscado.fetch()retornanullpara uma publicação pausada ou revertida, um manifesto cuja listaplatformsnão incluiweb, um formato de manifesto mais novo ou qualquer erro de rede.- O modo de manutenção tem prioridade:
apply()então cobre a página inteira com o texto de manutenção. Um aviso dispensado permanece oculto até que um novoannouncementIdseja publicado.
appmintNative.requestDndAccess appmintNative#
requestDndAccess() — @/lib/appmintNative
Exemplo
Pede acesso a Não perturbe, que o Android exige antes de o app poder mudar o volume do toque ou das notificações enquanto Não perturbe está ativado.
Retorna: 'granted', 'opened' (a tela de configurações foi exibida - verifique de novo quando o usuário voltar), 'not_declared', ou 'unavailable' na web. Requer: nada para marcar - chamá-lo no seu código faz a compilação declarar a permissão.
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';
}Pergunte uma vez e depois tente de novo a mudança de volume após o usuário voltar das configurações:
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;
};
}Trate cada resposta que ele pode dar:
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>
Exemplo
Pede ao Google Play que mostre a própria janela de avaliação dentro do app (Play In-App Review).
Retorna: Promise<boolean> - true somente quando o Play mostrou a folha; false na web, em uma instalação que não veio do Google Play, quando o Play recusou (sua cota) ou falhou. Use askForReview() para saber qual foi o caso. Requer: nada; a folha só aparece em um app instalado pelo 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();
}
}Descubra por que a folha não apareceu, sem mandar o usuário embora:
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);
}Pergunte apenas depois de um momento de sucesso e no máximo uma vez por mês:
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();
}Observações: O Google Play limita a frequência da folha e muitas vezes não mostra nada - nunca recompense uma avaliação e nunca crie um botão "Avalie-nos" que espera que a folha abra (use askForReview({ openStore: true }) para um botão). Ela nunca leva o usuário para fora do app até a página no Play.
appmintNative.restorePushMessage appmintNative#
import { restorePushMessage } from '@/lib/appmintNative'
Exemplo
Desfaz dismissPushMessage(id) — a mensagem volta à caixa de entrada do AppMint Push no lugar de antes.
Retorna: boolean de forma síncrona — true se foi restaurada; false se o id é desconhecido ou não foi dispensado (sempre false na web). Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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();
}Ofereça um Desfazer logo após deslizar para remover, em uma pequena linha 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>
);
}Verifique a resposta, porque um ID desconhecido ou nunca dispensado 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)`;
}Observações: as mensagens removidas com clearPushInbox() ou retiradas por você não podem ser restauradas.
appmintNative.safeArea appmintNative#
safeArea(): { top: number; right: number; bottom: number; left: number }
Exemplo
Informa o espaço ocupado pela barra de status, pelo recorte da câmera e pela barra de navegação, em pixels CSS, para que barras fixas não fiquem embaixo deles.
Retorna: { top, right, bottom, left } números, de forma síncrona. Requer: nada.
Na maioria das vezes o CSS basta — o runtime define --safe-top, --safe-right, --safe-bottom, --safe-left em :root:
export function BottomNav() {
return (
<nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
{/* tabs */}
</nav>
);
}Use os números quando o código precisar fazer contas, por exemplo para posicionar um botão flutuante:
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;
}Mantenha uma lista com rolagem longe das barras definindo o espaçamento interno a partir dos números:
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>;
}Observaçþes:
- Medido a partir do CSS
env(safe-area-inset-*). O valor fica em cache e é medido de novo após redimensionar ou girar a janela. - Antes de
document.bodyexistir (muito cedo, no momento da importação), retorna tudo zero - chame-o a partir de um effect. - Em um navegador de computador, os quatro costumam ser
0.
appmintNative.saveFile appmintNative#
saveFile(fileName, data, mime?) from '@/lib/appmintNative'
Exemplo
Salva texto ou um Blob como um arquivo que o usuário guarda, com o nome que você escolher. No app abre a janela "Salvar como…" do Android com esse nome; em um navegador é um download normal.
Retorna: (síncrono) boolean - true quando o download foi iniciado, false quando não há document ou sua criação lançou um erro. Não informa se o usuário salvou ou cancelou a folha. Requer: nada.
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.');
}Um Blob mantém o próprio tipo (o terceiro argumento é ignorado):
import { saveFile } from '@/lib/appmintNative';
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
saveFile('Backup.json', blob);Salve o resultado de uma busca (fetch) e avise quando ela não pôde começar:
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');
}Observações: É um <a download> simples por baixo, que o app transforma na folha "Salvar como…" (veja downloads). Sempre passe um nome com extensão; sem ela, o arquivo é salvo como download. O tipo MIME padrão para uma string é text/plain.
appmintNative.saveToGallery appmintNative#
import { saveToGallery } from '@/lib/appmintNative'
Exemplo
Salva uma foto ou vídeo na galeria do celular - can('gallery') é 'native' no app Android e 'web' em um navegador (o arquivo é baixado lá).
Retorna: Promise da URI do item salvo ('' na web). Rejeita com code 'permission-denied', 'download-failed', 'bad-source' ou 'failed'. Requer: nada; sem permissão no 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>
);
}Salve um videoclipe e trate os códigos de falha com que ele pode rejeitar:
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');
}
}Salve uma imagem a partir de uma data URL e entregue-a à folha de compartilhamento somente se tiver sido salva:
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' });
}Observações: source também pode ser uma URL data: ou uma URL http(s); passe { kind: 'video' } para uma URL de vídeo sem tipo MIME de vídeo.
appmintNative.scanBarcode appmintNative#
scanBarcode(video: HTMLVideoElement, opts?: { signal?, formats?, facingMode? }): Promise<{ value, format } | null>
Exemplo
Abre a câmera em um <video> que VOCÊ posiciona no seu layout e resolve com o primeiro código de barras ou QR code que ela vê.
Retorna: Promise<{ value: string; format: string } | null> — o primeiro código, ou null quando cancelado (o signal foi abortado), quando a câmera foi recusada ou quando o escaneamento não está disponível. Nunca rejeita e sempre para a câmera. Requer: acesso à câmera — no app Android ative Câmera na Etapa 4 (Acesso) ao compilar.
Use apenas quando o visor precisar ficar dentro da sua própria tela; caso contrário, scanCode() é mais simples.
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>
);
}Use a câmera frontal:
import { scanBarcode } from '@/lib/appmintNative';
declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });Feche o scanner após a primeira leitura e ofereça um código digitado quando a câmera não estiver disponível:
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>
);
}Observaçþes:
- Chama
getUserMedia(assim o usuário vê o aviso de permissão da câmera) e decodifica comBarcodeDetectorem cada quadro.can('barcodeScan')é'none'onde qualquer um deles estiver ausente. - A compilação examina seu código: uma chamada
scanBarcode(ativa Câmera para você. Se você a desativou manualmente, o pedido de câmera falha e você recebenull. - Inicie a partir de uma ação do usuário (um botão Escanear ou a abertura de uma tela de escaneamento), nunca ao abrir o app. Trate
nullcomo um resultado normal, com uma opção de entrada manual.
appmintNative.scanCode appmintNative#
scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>
Exemplo
Abre um escâner em tela cheia e resolve com o primeiro código de barras ou QR - uma chamada, nada para desenhar.
Retorna: Promise<ScanCodeResult | null> - { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }, ou null quando o usuário fechou o scanner. Qualquer outra coisa rejeita com um Error cujo code é 'unavailable' (sem scanner aqui - falta o Play Services no app; sem BarcodeDetector ou câmera na web), 'busy' (um escaneamento já está aberto), 'camera-denied' (web: permissão da câmera recusada) ou 'failed'. Requer: nada no app (o scanner do Google não precisa de permissão de câmera).
A partir de um botão Escanear:
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>
);
}Use as partes interpretadas que o app preenche (Wi-Fi, links, contatos …), com uma alternativa digitada no scanner do 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);Adie a partir de um botão na sua própria tela de alarme, usando onAlarm para saber quando ele está tocando.
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 responde false quando nada está tocando, então verifique antes de prometer ao usuário um segundo toque.
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.';
}Observaçþes:
- No app, este é o escâner de códigos do Google (Play services, por meio do
AppMint.scanCodedo shell): zoom automático, sem pedido de câmera.typeé'url','wifi','text','phone','email','sms','geo','contact','calendar','isbn','product'ou'driver_license', e o campo correspondente é preenchido.manualInputdeixa o usuário digitar o código. - Na web, desenha sua própria camada de câmera em tela cheia (botão de fechar, o gesto de voltar a fecha) e decodifica com
BarcodeDetector; apenasvalueeformatsão preenchidos ali, e o navegador pede a permissão da câmera. nullsempre significa que o usuário fechou o escâner; qualquer outro resultado rejeita com umcode, então mostre uma mensagem emcatch.- Chame a partir de um toque, nunca ao carregar a página. Restrinja
formatsquando os conhecer.
appmintNative.scheduleAlarm appmintNative#
scheduleAlarm(id, input) — @/lib/appmintNative
Exemplo
Define um despertador de verdade a partir de um app criado com IA: ele toca no volume de alarme do celular (mesmo no modo silencioso e no Não Perturbe, quando o usuário permite alarmes), continua tocando até Dispensar / Adiar ou ringSeconds, e coloca o app na tela sobre a tela de bloqueio.
Retorna: boolean, de forma síncrona — true quando definido; false na web, no iOS ou quando alarmSupport() não é 'ready'. O mesmo id substitui um alarme anterior. Requer: 🔔 Despertador na etapa Acesso (Etapa 4) ao compilar.
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';
}Ofereça o alarme somente quando o app puder tocar um e explique o que falta caso contrário:
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';
}Reaja a como um alarme que estava tocando terminou e interrompa-o a partir da sua própria tela:
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
};
}Observações: Somente no app Android. repeat diário / semanal / dias úteis / mensal / anual mantêm o mesmo horário ao longo das mudanças de horário de verão. Cancele com cancelNotification(id) na ponte; os resultados chegam por onAlarm.
appmintNative.ScreenOrientation appmintNative#
ScreenOrientation.lock({ orientation }) / ScreenOrientation.unlock()
Exemplo
Fixa a tela em retrato ou paisagem para uma tela, no formato do plugin ScreenOrientation do Capacitor — o mesmo código de lockOrientation().
Retorna: Promise<void> para ambos. lock rejeita, como o do Capacitor, quando orientation não é um tipo de bloqueio ou a plataforma recusa o bloqueio. Requer: nada.
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" />;
}Trave a orientação para uma tela apenas quando o bloqueio for permitido e deixe o usuário girar de volta:
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 precisar saber se o bloqueio funcionou, use lockOrientation() diretamente (ele responde false onde o bloqueio é recusado, como em navegadores de 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');
}Observações: orientation é um dos tipos de bloqueio do Capacitor — 'any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'portrait-secondary', 'landscape-primary', 'landscape-secondary' — e é aplicado como informado. Navegadores de desktop recusam bloqueios; navegadores de celular geralmente só permitem em tela cheia. lockOrientation(to) retorna true/false. Dê ao usuário uma saída de uma tela bloqueada.
appmintNative.screenReader appmintNative#
screenReader.isEnabled() / screenReader.speak(text, lang?) / screenReader.onChange(cb)
Exemplo
Informa ao app se o TalkBack está ativado e anuncia mensagens curtas.
Retorna: isEnabled() → Promise<boolean> (false na web - não existe API web). speak() → Promise<void>. onChange(cb) → uma função para cancelar a assinatura; cb(enabled) é executado quando o leitor de tela é ligado ou desligado (somente no app). Requer: nada.
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');
}Anuncie um resultado apenas para quem usa o leitor de tela e deixe os toasts visuais para todos os outros:
import { screenReader, toast } from '@/lib/appmintNative';
export async function notifySaved() {
if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
else toast('Note saved');
}Remova uma animação enquanto o TalkBack estiver ativado e pare de ouvir quando o componente sair:
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>;
}Observações: No app, speak passa pelo plugin ScreenReader; na web (ou se o plugin falhar) é falado com texto para fala. Prefira uma boa marcação (regiões aria-label, aria-live) para a maioria dos anúncios; use speak para mudanças curtas e importantes.
appmintNative.searchContacts appmintNative#
searchContacts(query: string, limit = 50): Promise<Contact[]>
Exemplo
Encontra contatos que correspondem a uma palavra de busca. Somente no app.
Retorna: Promise<Contact[]> - vazio quando nada corresponde, o acesso é recusado ou Contatos está desativado. Requer: o mesmo que listContacts: Contatos na Etapa 4 (Acesso) (ativado pela compilação quando seu código chama isto) e a permissão do usuário no primeiro uso.
Caixa de busca com um pequeno atraso enquanto se digita.
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);
}Mostre um estado claro quando Contatos não está disponível e quando nada corresponde:
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>
);
}Procure uma pessoa pelo nome e depois preencha previamente uma mensagem de texto para ela:
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
}Observações: a correspondência é feita pela busca de contatos do celular por nomes. Cada contato é { id?, name?, phones?: string[], emails?: string[], photoUri? }. Na web can('contactList') é 'none' — não há API web para ler a agenda. Contatos são dados pessoais: declare-os no seu formulário de Segurança dos dados do Google Play e na política de privacidade.
appmintNative.SecureKeyError appmintNative#
class SecureKeyError extends Error { code } — from '@/lib/appmintNative'
Exemplo
O erro que secureKeys gera. O code dele é uma palavra curta na qual você pode se basear, igual no app e na web.
Retorna: um Error com name === 'SecureKeyError' e code: 'no-such-key', 'auth-required' (desbloqueie e tente de novo), 'key-invalidated' (a biometria ou o bloqueio de tela mudou — os dados se perderam), 'strongbox-unavailable', 'disabled' (Chaves seguras ausentes desta compilação / sem Web Crypto), 'bad-alias', 'bad-payload' (base64 inválido ou curto demais), 'too-large' (mais de 4 MB em uma chamada no Android), 'foreign-origin' (uma página de outro site fez o pedido), 'failed' (qualquer outro caso). Requer: nada.
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;
}
}
}Transforme o código em uma mensagem curta para o usuário em um só lugar:
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 + ')';
}
}Trate um payload adulterado ou errado como um resultado normal e limpe-o, mas mantenha uma chave ausente como um erro que você exibe:
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;
}
}Observações: e.message é code ou "code: detail"; ramifique por e.code, nunca pela mensagem. As palavras de erro das próprias plataformas são convertidas nesses códigos, e o texto original é mantido na mensagem. Um payload alterado ou de outra origem é 'bad-payload' na web e 'failed' no app, que não consegue distingui-lo de outros erros de cifra.
appmintNative.secureKeys appmintNative#
secureKeys — generate / encrypt / decrypt / remove / has / list / isHardwareBacked
Exemplo
Chaves de criptografia que o aparelho guarda e que o app nunca pode exportar: o Android Keystore no app e uma chave Web Crypto não extraível na web. Remover a chave é a única "exclusão segura" de verdade.
Retorna: todas são Promises. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked}; encrypt(alias, text) → string base64; decrypt(alias, base64) → o texto; remove(alias) / has(alias) / isHardwareBacked(alias) → booleano; list() → string[]. generate, encrypt e decrypt geram SecureKeyError com um code. Requer: Chaves seguras (criptografia) na Etapa 4 (Acesso) ao compilar.
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;
}
}Exclua de verdade - ninguém poderá ler os dados selados depois disso:
import { secureKeys } from '@/lib/appmintNative';
export async function wipeDiary() {
await secureKeys.remove('diary');
localStorage.removeItem('diary');
}Liste as chaves que o app guarda e mostre se cada uma é protegida por hardware (a web sempre responde 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'}`));
}Observações: can('secureKeys') é 'native' no app e 'web' em um navegador, onde isHardwareBacked() é sempre false - nunca diga "hardware" ali. Uma chave requireAuth: no app, encrypt/decrypt lançam auth-required sem mostrar nada depois que a janela de desbloqueio passou - chame unlock() e tente de novo; na web, o runtime mostra unlock() sozinho. Criptografe um registro por vez (o app limita uma chamada a 4 MB). Aliases: letras, dígitos, _ - ., no máximo 64.
appmintNative.sendSms appmintNative#
sendSms(phoneNumber: string, message: string): Promise<boolean>
Exemplo
Envia um SMS diretamente, sem tela de confirmação. Somente no app Android.
Retorna: Promise<boolean> — true quando o celular informa que a mensagem foi enviada, false quando falhou, quando o SMS não estava ativado ou quando o usuário recusou a permissão. Requer: a opção SMS (ler e enviar) na Etapa 4 (Acesso) — a compilação a ativa quando o seu código chama isto — e a permissão do usuário no primeiro uso.
Envie um alerta e diga ao usuário o que aconteceu.
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';
}
}Peça uma confirmação antes, já que a mensagem sai sem nenhuma tela para cancelar:
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.');
}Envie o mesmo alerta para várias pessoas, uma de cada vez, e conte o que foi enviado:
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 };
}Observações: o wrapper resolve na PRIMEIRA resposta, que é sent (a mensagem saiu do celular), não delivered. Para relatórios de entrega use o WebToApk.sendSms bruto com o evento appmint:sms. can('smsSend') é 'none' na web (nenhum navegador pode enviar SMS). Cada SMS pode custar dinheiro ao usuário. SMS é uma permissão restrita do Google Play: você precisa de uma Declaração de Permissões aprovada no Play Console, então prefira composeSms() quando o usuário puder apertar enviar.
appmintNative.setInAppPresentation appmintNative#
import { setInAppPresentation } from '@/lib/appmintNative'
Exemplo
Escolhe quem desenha uma mensagem no app (as campanhas do criador): o app ('auto', o padrão: o design de banner, cartão, folha ou tela cheia do criador) ou a sua própria interface ('none', via onInAppMessage).
Retorna: boolean de forma síncrona: true quando salvo (fica no celular). Sempre false na web e sem o AppMint Push. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
import { setInAppPresentation } from '@/lib/appmintNative';
// The app draws its own messages from now on.
setInAppPresentation('none');Um botão de configurações:
import { setInAppPresentation } from '@/lib/appmintNative';
export function onNativePopupsChange(on: boolean) {
if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}Assuma o controle enquanto uma tela estiver aberta e depois devolva-o:
import { useEffect } from 'react';
import { setInAppPresentation } from '@/lib/appmintNative';
export function useQuietScreen() {
useEffect(() => {
setInAppPresentation('none');
return () => { setInAppPresentation('auto'); };
}, []);
}Observações: uma mensagem entregue ao seu código conta como exibida. Definir 'none' também fecha agora uma que esteja na tela.
appmintNative.setMusicVolume appmintNative#
setMusicVolume(percent) — @/lib/appmintNative
Exemplo
Define o volume da música de fundo em loop do app, de 0 a 100 (arredondado e limitado automaticamente).
Retorna: void. Requer: um arquivo escolhido em Música de fundo na Etapa 3 (Integrar) ao compilar. Não faz nada quando musicVolume() é -1.
O app guarda o nível para a próxima abertura, então um botão de silenciar é tudo de que você precisa:
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';
export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40); // remembered by the appUm controle de volume que aparece apenas quando o app inclui música:
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); }} />
);
}Abaixe a música enquanto um vídeo toca e volte depois:
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; }Observações: 0 deixa a música em silêncio, mas ela continua tocando. O nível é salvo no celular (até o usuário limpar os dados do app). Somente no app Android; na web nada acontece.
appmintNative.setPushPresentation appmintNative#
import { setPushPresentation } from '@/lib/appmintNative'
Exemplo
Escolhe quem desenha uma mensagem do AppMint Push que você enviou com estilo dentro do app (banner, pop-up, bottom sheet ou tela cheia) enquanto o app está aberto: o shell do app ('auto', o padrão) ou o seu próprio código ('none').
Retorna: boolean de forma síncrona - true quando o modo foi salvo. Sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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();
}Verifique a resposta, pois ela é false na web ou quando o AppMint Push está desativado:
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;
}Desenhe seu próprio banner para cada mensagem e mantenha a caixa de entrada sincronizada:
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); } };
}Observações: a caixa de entrada e o evento appmint:push funcionam igual nos dois modos. Definir 'none' também fecha uma mensagem que o shell esteja mostrando agora.
appmintNative.setPushTag appmintNative#
import { setPushTag } from '@/lib/appmintNative'
Exemplo
Define uma tag do AppMint Push nesta instalação. A central de envios pode enviar para todos que têm uma tag, e uma automação por tag pode enviar uma mensagem um tempo definido depois que a tag é definida. '' remove a tag.
Retorna: boolean de forma síncrona - true quando salvo; false quando a chave não tem de 1 a 32 letras, dígitos ou _, o valor passa de 128 caracteres ou a instalação ultrapassaria 50 tags. Sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar; a segmentação por tags e as automações alcançam apps compilados a partir de 30 de setembro de 2026.
import { useEffect } from 'react';
import { setPushTag } from '@/lib/appmintNative';
// Abandoned cart: a tag automation on "cart" (say 60 minutes) reminds only phones whose cart is
// still open when the time is up. Setting 'open' again does not restart the clock.
export function useCartPushTag(itemCount: number) {
const hasItems = itemCount > 0;
useEffect(() => {
setPushTag('cart', hasItems ? 'open' : '');
}, [hasItems]);
}
export function onCheckoutComplete() {
setPushTag('cart', '');
}Verifique o resultado: false significa que a tag foi recusada (chave inválida, valor com mais de 128 caracteres, mais de 50 tags) ou que você está na web, então mantenha seu próprio registro do estado.
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;
}Marque um celular quando ele atingir um marco, para a central de envios poder alcançar todos nesse nível.
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]);
}Observações: as tags pertencem à instalação, não ao usuário logado. Nunca coloque dados privados em uma tag.
appmintNative.setPushTags appmintNative#
import { setPushTags } from '@/lib/appmintNative'
Exemplo
Define várias tags do AppMint Push em uma única alteração; um valor '' remove aquela tag, e as tags que você não nomear são mantidas.
Retorna: boolean de forma síncrona - true quando salvo; false quando qualquer chave ou valor viola as regras de setPushTag (então nada é salvo). Sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar; segmentação por tags e automações alcançam apps compilados a partir de 30 de set. de 2026.
import { setPushTags, getPushTags, setPushUser } from '@/lib/appmintNative';
export function tagProfile(profile: { plan: 'free' | 'pro'; level: number; team?: string }) {
return setPushTags({
plan: profile.plan,
level: String(profile.level), // values are text
team: profile.team ?? '', // '' removes the tag
});
}
// Sign-out: clear every tag and the user id.
export function signOutOfPush() {
const cleared = Object.fromEntries(Object.keys(getPushTags()).map((k) => [k, '']));
setPushTags(cleared);
setPushUser('');
}Marque um celular com o idioma e os tópicos escolhidos quando o usuário salvar suas configurações e verifique se todas as tags foram aceitas.
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;
}Mantenha as tags alinhadas com a conta logada em um effect do React, removendo as que não se aplicam mais.
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]);
}Observações: tudo de uma chamada é salvo ou nada é salvo.
appmintNative.setPushUser appmintNative#
import { setPushUser } from '@/lib/appmintNative'
Exemplo
Informa ao AppMint Push qual dos seus usuários está conectado nesta instalação, para a central de envios poder enviar uma mensagem a esse usuário. Passe o seu próprio id de conta estável após o login e '' ao sair.
Retorna: boolean de forma síncrona - true quando o id foi salvo; false para um id com qualquer coisa além de letras, dígitos e _ @ . : + - (até 128). Sempre false na web. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar; o envio para um usuário alcança apps compilados a partir de 30 de setembro de 2026.
import { useEffect } from 'react';
import { setPushUser, getPushUser } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';
// Keeps AppMint Push in step with the signed-in account: the auth user id, never the email.
export function usePushUser() {
useEffect(() => {
const { data } = supabase.auth.onAuthStateChange((_event, session) => {
const id = session?.user.id ?? '';
if (getPushUser() !== id) setPushUser(id);
});
return () => data.subscription.unsubscribe();
}, []);
}Trate um ID recusado: setPushUser responde false para um endereço de e-mail ou qualquer ID com caracteres fora do conjunto permitido, então envie um ID estável.
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;
}Limpe o ID quando o usuário sair da conta, em um manipulador de clique simples.
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>
);
}Combine com uma tag para que uma única etapa de login identifique o usuário e o plano dele juntos.
import { setPushUser, setPushTag } from '@/lib/appmintNative';
export function onSignedIn(userId: string, plan: 'free' | 'pro') {
setPushUser(userId);
setPushTag('plan', plan);
}Observações: o ID pertence à instalação, então sair da conta deve limpá-lo. Ele não limpa as tags - chame setPushTags com valores '' para as específicas do usuário.
appmintNative.setStatusBar appmintNative#
setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void
Exemplo
Pinta a barra de status do sistema para combinar com a tela e escolhe ícones claros ou escuros para ela.
Retorna: nada. Requer: nada.
style é a cor dos ÍCONES: 'light' = ícones claros para uma barra escura (o padrão), 'dark' = ícones escuros para uma barra clara. Chame uma vez na inicialização com o --bg do app e de novo em qualquer tela com fundo diferente:
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;
}Uma tela clara:
import { setStatusBar } from '@/lib/appmintNative';
setStatusBar('#FFFFFF', 'dark');A forma no estilo Capacitor (o estilo recebe o nome do FUNDO: 'DARK' = ícones claros):
import { StatusBar } from '@/lib/appmintNative';
await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });Observaçþes:
- No app Android, o shell pinta a barra por conta própria, o que também funciona no Android 15 edge-to-edge; uma cor que não pode ser interpretada é ignorada - passe
#RRGGBB. Uma chamada só de estilo (backgroundColor = null) mantém a cor que esta página definiu por último e inverte os ícones; antes de a página definir qualquer cor, ela inverte os ícones pelo plugin StatusBar e a barra mantém a cor escolhida na compilação. - Na web, define
<meta name="theme-color">; o navegador escolhe sozinho a cor dos ícones.
appmintNative.setVolume appmintNative#
setVolume(stream, percent, showUi?) — @/lib/appmintNative
Exemplo
Define o nível de volume do próprio celular para um canal, de 0 a 100. showUi mostra o controle de volume do celular enquanto ele se move.
Retorna: { ok, stream?, current?, max?, percent?, error? } — o erro é unknown_stream, bad_percent, fixed_volume, dnd_access_required (o Não Perturbe bloqueia toque / notificação — chame requestDndAccess()), muted (o "Silêncio total" do Não Perturbe silencia o canal, então o nível não mudou), not_applied, foreign_origin, failed ou unavailable na web. Requer: nada para mídia, alarme e chamada.
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();
}}
/>
);
}Mostre o volume de mídia como um controle deslizante que acompanha as teclas físicas, 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))} />;
}Aumente o volume do canal de alarme antes de uma tela de alarme tocar e informe por que ele não mudou.
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 }
Exemplo
Abre uma folha modal inferior com comportamento real de folha - para um painel de filtros, um editor de mensagem ou uma prévia de detalhes.
Retorna: um handle: close() fecha, element é o elemento do corpo (preencha ou atualize), closed é uma Promise<void> que resolve quando a janela fechou, seja como for. Requer: nada.
Opções: title (título opcional), content — um elemento, texto simples (não HTML) ou uma função (host) => { … } que preenche o corpo; dismissible (padrão true: toque no fundo, arrastar para baixo, Escape e o gesto de voltar fecham).
Conteúdo DOM simples:
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;Conteúdo React, renderizado na folha com um 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,
)}
</>
);
}Uma janela que fica aberta até o usuário escolher, usando dismissible: false e a 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;
}Observaçþes:
- Desenhado com os tokens do app (
--surface,--ink,--muted,--border), acima da área segura, com um leve toque háptico ao abrir. - Arrastar para baixo fecha quando arrastado além de 120 px ou com um gesto rápido; o corpo rola primeiro quando não está no topo.
- Com
dismissible: false, somente o seu próprioclose()fecha — dê um botão ao usuário. - Para uma lista simples de ações, use
actionSheet().
appmintNative.shortcuts appmintNative#
shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()
Exemplo
Adiciona atalhos ao menu de toque longo do ícone do app (atalhos dinâmicos do Android). Um toque abre o app naquela rota.
Retorna: set() retorna Promise<boolean> - true quando os atalhos foram entregues ao sistema, false onde não há nenhum (a web). clear() retorna Promise<void>. Requer: nada.
Defina-os uma vez na inicialização e direcione pelo link de abertura:
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]);
}Remova-as (por exemplo, depois de sair da conta):
import { shortcuts } from '@/lib/appmintNative';
await shortcuts.clear();Abra a tela correspondente quando um atalho for tocado com o app já em execução, com 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]);
}Mostre atalhos somente para um usuário conectado e trate a web, onde set() responde 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]);
}Observaçþes:
- No máximo 4 itens; itens extras são descartados. Um
routesem/inicial recebe um.idé opcional. - Android 7.1+; em versões mais antigas do Android a chamada não faz nada. Os títulos são cortados em 25 caracteres (rótulo curto) e 50 (rótulo longo).
- Um toque abre o app com o link de abertura
appmint-shortcut://<package><route>, entãonew URL(u).pathnameé a sua rota. Leia comlaunchUrl()ao montar eonDeepLink()enquanto o app roda. can('shortcuts')é'none'na web: um navegador não tem ícone de app.
appmintNative.snoozeAlarm appmintNative#
snoozeAlarm(id?, minutes?) — @/lib/appmintNative
Exemplo
Para o alarme que está tocando e o faz tocar de novo depois de minutes (0 ou omitido = o snoozeMinutes do próprio alarme, padrão 10). O toque de amanhã de um alarme repetido não muda.
Retorna: boolean - true quando estava tocando e foi adiado. Requer: 🔔 Despertador na etapa Acesso (Etapa 4) ao compilar.
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);
});Adie a partir de um botão na sua própria tela de alarme, usando onAlarm para saber quando ele está tocando.
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 responde false quando nada está tocando, então verifique antes de prometer ao usuário um segundo toque.
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
Exemplo
Lê texto em voz alta com o mecanismo de texto para fala do celular.
Retorna: void - começa a falar (ou entra na fila depois da fala atual) e não dá sinal de "terminou". opts: { lang?: string; rate?: number; pitch?: number } (padrões: idioma do mecanismo, 1, 1). Requer: nada.
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>
);
}Idiomas e velocidades diferentes:
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 = normalFale uma confirmação curta depois de uma ação e avise o usuário também por texto.
import { speak, toast } from '@/lib/appmintNative';
export function announceSaved(title: string) {
toast('Saved');
speak(`${title} saved`, { rate: 1.1 });
}Leia uma lista item por item; speak() enfileira, então chame-o em um loop e interrompa tudo com stopSpeaking() quando a tela fechar.
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>;
}Observações: no app Android: o motor nativo do app. Na web: speechSynthesis. Várias chamadas de speak() tocam uma após a outra. Se precisar saber quando a fala termina (para destacar palavras ou seguir adiante), use window.speechSynthesis com o onend de uma fala diretamente; funciona também no app Android.
appmintNative.speech appmintNative#
speech — @/lib/appmintNative
Exemplo
Ditado: transforma a fala do usuário em texto. Usa o reconhecedor de voz do Android no app Android e a Web Speech API no Chrome.
Retorna: speech.isAvailable() → Promise<boolean>. speech.start(onResult, opts?) → Promise<boolean>: true quando a escuta começou, false quando a permissão do microfone foi recusada ou o ditado não está disponível. onResult(text, isFinal) é chamado com o texto. speech.stop() → Promise<void>. opts: { lang?: string; partial?: boolean; prompt?: string }. Requer: Microfone na Etapa 4 (Acesso); apps criados com IA ativam isso por você quando o código usa reconhecimento de voz.
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>
);
}Dite em um campo de texto e avise quando o microfone foi recusado, já que start() responde 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>
);
}Pare de escutar quando a tela fechar e leia o resultado de volta com 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>
);
}Observaçþes:
- Com
partialativado (o padrão),onResult(text, false)chega conforme as palavras vêm, e depois uma vezonResult(text, true)com o texto final. Compartial: falsevocê recebe apenas o final, quando o usuário para de falar. - O aviso de permissão é mostrado na primeira
start(). - Se o usuário não disser nada,
onResultnão é chamado; a escuta apenas termina. Mantenha um botão Parar ou um tempo limite na sua interface. - A escuta para sozinha após uma pausa.
langusa por padrão o idioma do celular.
appmintNative.splash appmintNative#
splash.hold() / splash.ready()
Exemplo
Mantém a tela de abertura na tela até que a primeira tela real esteja pronta, em vez de mostrar "abertura e depois um indicador de carregamento".
Retorna: nada (ambos são síncronos). Requer: nada.
Por padrão, a tela de abertura se oculta sozinha na primeira renderização do app - você não precisa de nada. Quando a primeira tela precisa de dados, segure a tela de abertura no topo do módulo e libere-a quando essa tela estiver visível:
// 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>;
}Segure a tela de abertura enquanto uma sessão logada é restaurada e libere-a em um finally para que uma falha não a mantenha na tela.
// main.tsx
import { splash } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';
splash.hold();
supabase.auth.getSession().finally(() => splash.ready());Libere-a uma vez, a partir de um hook, depois que os dados da primeira tela estiverem na tela.
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]);
}Observaçþes:
- No app Android,
hold()diz ao shell para manter a tela de abertura;ready()a oculta (o tempo mínimo de abertura do criador ainda vale). - Na web, ambos não fazem nada - não há tela de abertura.
- O shell limita a espera a cerca de 6 segundos, então uma página que nunca chama
ready()ainda assim abre.
appmintNative.StatusBar appmintNative#
StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })
Exemplo
A barra de status no formato do plugin StatusBar do Capacitor, construída sobre setStatusBar().
Retorna: Promise<void> nos dois casos. Requer: nada.
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' });Cor e ícones juntos - prefira setStatusBar(), que define os dois em uma chamada:
import { setStatusBar } from '@/lib/appmintNative';
setStatusBar('#0B0F1A', 'light'); // dark bar, light icons
setStatusBar('#FFFFFF', 'dark'); // light bar, dark iconsAlterne a barra com o tema claro e escuro do próprio app, usando as chamadas no formato Capacitor em conjunto.
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]);
}Observações: style segue o Capacitor: 'DARK' significa um fundo escuro (ícones claros), qualquer outro valor significa ícones escuros. No app Android, a ponte do shell pinta a barra (a chamada de cor do plugin não faz nada no Android 15+), e setStyle() inverte os ícones conforme a cor atual da barra. Na web, a cor se torna <meta name="theme-color">.
appmintNative.stopAlarm appmintNative#
stopAlarm(id?) — @/lib/appmintNative
Exemplo
Para o despertador que está tocando, como o botão Dispensar dele. Um alarme repetido continua programado para a próxima vez.
Retorna: boolean - true quando estava tocando e agora parou. id omitido = o que estiver tocando. Requer: 🔔 Despertador na etapa Acesso (Etapa 4) ao compilar.
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>
);
}Pare o alarme a partir da tela que o app abre quando o alarme toca e reaja a todas as formas de ele terminar:
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 ?? ''); },
};
}Verifique a resposta: false significa que nada estava tocando (ou que você está na web), então não diga que parou:
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
Exemplo
Interrompe o texto para fala agora e descarta tudo o que ainda aguarda para ser falado.
Retorna: void. Requer: nada.
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>
);
}Leia um texto longo em voz alta com o idioma e a velocidade escolhidos pelo usuário e pare a leitura com um botão:
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>
);
}Corte a voz quando o usuário começar a digitar uma resposta:
import { stopSpeaking } from '@/lib/appmintNative';
export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}Observações: Seguro chamar quando nada está sendo falado. No app Android: a parada do mecanismo nativo. Na web: speechSynthesis.cancel().
appmintNative.swipeable appmintNative#
swipeable(el: HTMLElement, opts: SwipeOptions): () => void
Exemplo
Adiciona gestos de deslizar a um elemento - linhas de deslizar para excluir, paginação, dispensar um cartão.
Retorna: uma função para cancelar a assinatura que também restaura o touch-action original do elemento. Requer: nada.
Opções: onLeft, onRight, onUp, onDown (pelo menos uma), threshold (px de deslocamento, padrão 56), edgeGuard (px a partir da borda esquerda/direita da tela em que deslizes horizontais são ignorados, padrão 24), haptics (háptico de seleção ao deslizar, padrão true).
Deslize para a esquerda para excluir, com um botão visível para a mesma ação:
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>
);
}Alterne entre abas com esquerda / direita:
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();Dispense um cartão deslizando-o para cima, com o handler removido ao desmontar:
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>;
}Observaçþes:
- Baseado em Pointer Events, então o mesmo código funciona com toque, caneta e arrastar com o mouse.
- Define
touch-actionpara você:pan-ypara handlers horizontais (a rolagem vertical continua funcionando),pan-xpara verticais,nonepara ambos. - Um deslizar precisa ser claramente em uma direção (1,5 vez mais em um eixo); rolagens diagonais não disparam. Um segundo dedo cancela o gesto.
- A proteção de borda existe porque o gesto de voltar do sistema Android controla as bordas da tela.
appmintNative.systemPalette appmintNative#
systemPalette(): Promise<SystemPalette>
Exemplo
Lê as cores Material You do celular (geradas a partir do papel de parede no Android 12+), para que o destaque do app possa seguir o papel de parede do usuário como os apps do sistema.
Retorna: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - cada amostra associa um tom ('0', '10', '50', '100' … '900', '1000') a uma cor hexadecimal. { available: false } abaixo do Android 12 e na web. Requer: nada.
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']);
}Use as cores do papel de parede somente quando o celular as oferecer e leia-as de novo quando o app voltar para a tela:
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)
}Mostre a opção "Combinar com meu papel de parede" somente quando houver uma paleta, usando { available: false } como a resposta para "não aqui":
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>;
}Observações: verifique o contraste antes de usar um tom do papel de parede em textos ou botões — --accent ainda precisa atingir 3:1 sobre o seu fundo. Ofereça isso como opção ("Combinar com meu papel de parede"), não como tema forçado. Chame de novo quando o app for retomado; o usuário pode ter trocado o papel de parede.
appmintNative.TextToSpeech appmintNative#
TextToSpeech — @/lib/appmintNative
Exemplo
O objeto TextToSpeech no estilo Capacitor (speak / stop), rodando sobre o mesmo código de speak() e stopSpeaking().
Retorna: speak({ text, lang?, rate?, pitch? }) → Promise<void> que resolve quando o texto foi falado (ou stop() o interrompeu) e rejeita quando não pôde ser falado. stop() → Promise<void>. Requer: nada.
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();
}Leia em voz alta uma lista de linhas, uma após a outra; cada speak resolve quando enfileirado, então o mecanismo as reproduz em ordem:
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' });
}
}Um botão de leitura em voz alta que para de falar quando a tela é deixada:
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>;
}Observações: await TextToSpeech.speak(…) é o sinal de término: o próximo passo depois da fala pode segui-lo diretamente. No app: o mecanismo nativo de texto para fala do Android no shell; na web: speechSynthesis.
appmintNative.textZoom appmintNative#
textZoom.get() / textZoom.getPreferred() / textZoom.set(value)
Exemplo
Um controle de tamanho do texto dentro do app. No app, ele altera o zoom do texto da página (1 = 100%); na web, altera o tamanho da fonte raiz, que escala todo layout baseado em rem.
Retorna: get() → Promise<number>. getPreferred() → Promise<number> — o tamanho escolhido pelo usuário nas configurações do celular (1 na web). set(value) → Promise<void>; o valor é limitado a 0,5-3 e rejeita um valor que não seja número. Requer: nada.
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()); }Lembre a escolha entre aberturas:
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));
}Um seletor de tamanho de texto que lê o valor atual, o mantém dentro do intervalo permitido e o mostra em porcentagem:
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>
);
}Observações: o app já segue a configuração de tamanho de fonte do celular; este é um controle extra, dentro do app. Teste o layout em tamanhos grandes — alturas fixas em controles de texto cortam o conteúdo.
appmintNative.toast appmintNative#
toast(message: string, opts?: { duration?: 'short' | 'long' }): void
Exemplo
Mostra uma mensagem breve que não precisa de resposta - "Copiado", "Salvo", "Adicionado à lista".
Retorna: nada (dispara e esquece). Requer: nada.
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>
);
}Uma mensagem mais longa fica por cerca de 3,5 segundos:
import { toast } from '@/lib/appmintNative';
toast('Your export will be ready in a minute', { duration: 'long' });A forma no estilo Capacitor Toast.show({ text, duration? }) faz o mesmo e resolve Promise<void>:
import { Toast } from '@/lib/appmintNative';
await Toast.show({ text: 'Added to list' });Para uma mensagem que o usuário PRECISA ver, use uma caixa de diálogo:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });Observaçþes:
- No app Android, o plugin Toast do Capacitor o exibe na parte inferior da tela.
- Nos demais casos é uma pequena snackbar desenhada sobre a página nas cores do próprio app (
--inksobre--bg), acima da margem da área segura, comrole="status"para os leitores de tela a anunciarem. - Apenas um é exibido por vez; um novo toast substitui o antigo.
- Um toast é fácil de perder por design. Nunca o use para um erro que o usuário precisa resolver.
appmintNative.Toast appmintNative#
import { Toast } from '@/lib/appmintNative'
Exemplo
Documentado junto com appmintNative.toast - o exemplo de lá mostra este também.
Mostra uma mensagem breve que não precisa de resposta - "Copiado", "Salvo", "Adicionado à lista".
Retorna: nada (dispara e esquece). Requer: nada.
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>
);
}Uma mensagem mais longa fica por cerca de 3,5 segundos:
import { toast } from '@/lib/appmintNative';
toast('Your export will be ready in a minute', { duration: 'long' });A forma no estilo Capacitor Toast.show({ text, duration? }) faz o mesmo e resolve Promise<void>:
import { Toast } from '@/lib/appmintNative';
await Toast.show({ text: 'Added to list' });Para uma mensagem que o usuário PRECISA ver, use uma caixa de diálogo:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });Observaçþes:
- No app Android, o plugin Toast do Capacitor o exibe na parte inferior da tela.
- Nos demais casos é uma pequena snackbar desenhada sobre a página nas cores do próprio app (
--inksobre--bg), acima da margem da área segura, comrole="status"para os leitores de tela a anunciarem. - Apenas um é exibido por vez; um novo toast substitui o antigo.
- Um toast é fácil de perder por design. Nunca o use para um erro que o usuário precisa resolver.
appmintNative.torch appmintNative#
import { torch } from '@/lib/appmintNative'
Exemplo
A lanterna do celular: torch.on(), off(), toggle(), isOn(), isAvailable(). can('torch') é 'none' na web.
Retorna: on()/off() → Promise (rejeita com code 'unavailable' ou 'CAMERA_IN_USE'); toggle() → o novo estado; isOn() / isAvailable() → booleano. Requer: nada, nenhuma permissão.
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>
);
}Desligue a luz quando o usuário sair da tela e mostre o erro que o celular informa quando ela não pode ser ligada ou desligada:
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>;
}Pisque a luz como sinal e depois deixe-a apagada:
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));
}
}Observações: é desligada quando o app fecha. Enquanto o fluxo da câmera do próprio app está aberto, acenda-a com a restrição torch da trilha.
appmintNative.trackEvent appmintNative#
import { trackEvent } from '@/lib/appmintNative'
Exemplo
Informa um momento do seu app para que as mensagens dentro do app do criador com o gatilho "Quando um evento acontece" (mesmo nome) apareçam. O app baixa as campanhas do criador da central de envios do AppMint Push e as mostra por conta própria.
Retorna: boolean de forma síncrona: false para um nome inválido (letras, dígitos e _ . : -, até 64), na web e em apps compilados sem AppMint Push. Requer: Android: AppMint Push ativado na etapa Integrar (Etapa 3) ao compilar.
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
}Informe uma mudança de tela que a URL não mostra, para que as mensagens "Em uma página" a vejam:
import { trackEvent } from '@/lib/appmintNative';
export function selectTab(tab: 'home' | 'cart' | 'profile') {
setActiveTab(tab);
trackEvent('appmint:route', { path: '/' + tab });
}Um hook que registra a primeira vez que uma tela é aberta:
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]);
}Observações: mudanças de URL (carregamentos de página, pushState, rotas com #hash) já são detectadas; informe apenas o que o endereço não muda.
appmintNative.transition appmintNative#
transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>
Exemplo
Executa uma troca de tela com uma animação de aparência nativa: push desliza a nova tela vinda da direita, pop desliza de volta, fade faz uma transição suave.
Retorna: Promise<void> que é concluída após a mudança e sua animação. update é a sua mudança de estado (um navigate do roteador, um setState) e é sempre aplicada. Requer: nada.
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>;
}Transição suave entre duas abas:
import { transition } from '@/lib/appmintNative';
declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });Use um fade para trocar de aba, em um handler por aba:
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>
);
}Observaçþes:
- Usa a View Transitions API quando o WebView a possui. Caso contrário, aplica a atualização e executa uma breve animação de entrada em
#root(ou no primeiro filho de<body>). - Quando o usuário pediu ao sistema movimento reduzido, ele apenas aplica a atualização - sem animação.
- As animações duram cerca de 240 ms.
appmintNative.unlock appmintNative#
unlock(title?, subtitle?) — from '@/lib/appmintNative'
Exemplo
Pede ao dono do celular que prove que está presente — impressão digital, rosto ou PIN/senha do aparelho — e informa se ele conseguiu.
Retorna: Promise<boolean> — true quando desbloqueado; false quando cancelado, com falha, bloqueado por tentativas ou quando nada pode verificar neste aparelho. Nunca gera erro. Requer: no app, Impressão digital na Etapa 4 (Acesso) ao compilar. Na web: o autenticador de plataforma do navegador (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();
}Oculte o botão onde não há nada com que desbloquear:
import { can } from '@/lib/appmintNative';
const showLockButton = can('unlock') !== 'none'; // 'native' | 'web' | 'none'Proteja uma exclusão com um desbloqueio recente e depois confirme com um háptico e um 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');
}Observações: No app, o PIN, padrão ou senha do dispositivo é permitido como segunda forma de entrar. Na web, a primeira chamada registra o autenticador da plataforma do navegador e conta como desbloqueada. Isso comprova que o dono do dispositivo está presente; NÃO é um login - nunca use como única proteção de dados que ficam em um servidor. No Android 11 e mais novo, o prompt abre em qualquer celular com bloqueio de tela - um celular só com PIN também o recebe e desbloqueia com o PIN; no Android 10 e mais antigo, é preciso ter uma impressão digital cadastrada, senão responde false sem prompt.
appmintNative.updates appmintNative#
updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()
Exemplo
Um atualizador do lado da página para atualizações de pacote web. O código da página não precisa dele: em um app AppMint, a Atualização Remota é feita nativamente pelo app Android, e updates.supported() retorna false em todo lugar.
Retorna: supported() → boolean (atualmente sempre false); download(url, sha256, version, binaryVersion) → Promise<boolean> (false enquanto não houver suporte); stage(dir, version, binaryVersion) → Promise<boolean>; confirm() → void; revertIfUnconfirmed(binaryVersion) → Promise<boolean>; current() → { path, version, binaryVersion } | null. Requer: nada para o seu app — as atualizações são publicadas pelo Remote Update no AppMint.
O único uso seguro no código do app é verificar e não fazer nada quando estiver desativado:
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);
}Mostre qual bundle está em execução (uma linha "Sobre" nas configurações):
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';Mostre a versão do conteúdo em execução em uma linha de configurações, com o rótulo embutido quando a página não tem pacote preparado:
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>;
}Observaçþes:
- No app Android, o gerenciador de Atualização Remota do shell baixa, verifica e serve as atualizações, e reverte um pacote que falha ao carregar. A página é servida pelo próprio shell, então um pacote preparado a partir do código da página nunca seria exibido - por isso
supported()éfalse. - Na web não há nada para atualizar; a página é o que o servidor enviar.
- As atualizações podem mudar apenas HTML, CSS e JavaScript; um novo recurso nativo ainda exige uma nova compilação do app.
appmintNative.watchIdle appmintNative#
import { watchIdle } from '@/lib/appmintNative'
Exemplo
Avisa o app quando o usuário fica inativo ou bloqueia a tela, e quando volta (can('idle')). Inicie com um toque na primeira vez: o pedido de permissão exige isso.
Retorna: Promise da função de parada. O callback recebe { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } no início e a cada mudança. Rejeita com code 'permission-denied' ou 'unavailable'. Requer: nada, nenhuma permissão do 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>
);
}Mostre um estado de ausência e pare de monitorar ao desmontar; uma permissão recusada é informada com o seu 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 };
}Pause um timer de consulta enquanto o usuário está ausente e atualize quando ele voltar:
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); };
}Observações: o limite é de pelo menos um minuto (o mínimo da especificação).
appmintNative.widget appmintNative#
widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()
Exemplo
Preenche o widget da tela inicial do app — um cartão com título, um valor grande, uma legenda, até doze linhas (cada uma com cor, marcador e rota opcionais) e um toque que abre o app em uma rota.
Retorna: set(card) → Promise<boolean> (true quando salvo); clear() → Promise<void>; placedCount() → Promise<number> (quantas cópias o usuário colocou); requestPin() → Promise<boolean> (true quando o fluxo "adicionar widget" do launcher abriu). Nenhum deles rejeita. Requer: nada no app Android (o widget sempre faz parte da compilação).
Atualize o cartão após cada mudança que o usuário gostaria de ver na tela inicial:
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
});
}Uma agenda que preenche um widget redimensionado, com fundo transparente e uma linha de atraso em vermelho:
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',
});
}Um botão "Adicionar à tela inicial":
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>
);
}Esvazie o cartão quando o usuário sair da conta e mostre quantas cópias estão na tela inicial:
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>;
}Observaçþes:
- O cartão é salvo no aparelho, sobrevive a uma reinicialização e todas as cópias colocadas são atualizadas.
placedCount()igual a0significa que nenhuma cópia está na tela inicial ainda. - Campos do cartão:
title(vazio = o nome do app),value(exibido grande emaccent, oculto quando vazio),caption(texto pequeno abaixo do valor),lines(máx. 12; seis sempre aparecem, mais quando o widget é alto o suficiente; uma linha é uma string ou{ text, color?, marker?, bold?, wrap?, route? }),route(padrão/),textSize('auto'aumenta o texto junto com o widget até onde todas as linhas ainda couberem, quebrando linhas longas em duas;'small' | 'normal' | 'large' | 'xl'ou um número de 0,6 a 3),align('start' | 'center' | 'end'),wrap(linhas longas ocupam duas linhas) ebg/ink/accent. As cores usam por padrão os tokens CSS--bg,--ink,--accent; devem ser#RRGGBB,#AARRGGBB(alfa PRIMEIRO:#9914264Aé 60 % opaco),#RGB,rgb()/rgba()ou um nome de cor - um valorhsl()/oklch()volta às cores padrão.set()retornafalsepara umtextSize/aligndesconhecido ou uma cor de linha inválida. - Um toque abre o app com
appmint-shortcut://<package><route>; leia comlaunchUrl()/onDeepLink(). requestPin()requer Android 8+ e um launcher compatível.clear()mostra o cartão vazio "Nada para mostrar ainda".- Somente no app Android:
can('widget')é'none'na web, ondeset()erequestPin()retornamfalse.