Criar APK grátis

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; destructive usa 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 options vazia resolve null imediatamente. Para qualquer conteúdo que não seja uma lista de ações, use sheet().

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; destructive usa 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 options vazia resolve null imediatamente. Para qualquer conteúdo que não seja uma lista de ações, use sheet().

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, statusBar e swipe têm uma versão web de verdade, então não são 'none' em um navegador.
  • contactList, contactAdd, smsSend, smsList, callLog, shortcuts, shareTarget e widget sã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ão isNativeApp().
  • Um nome desconhecido retorna 'none'. Para um simples sim/não, use has(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.clearShared appmintNative#

clearShared(): void

Exemplo

Esquece o texto ou link que outro app compartilhou com este, depois que seu app o salvou.

Retorna: nada. Requer: nada (não faz nada na web).

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

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

Limpe o compartilhamento somente depois de salvo e mantenha-o quando o salvamento falhar:

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

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

Trate um compartilhamento que chega enquanto o app está aberto:

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

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

Observações: sharedText() mantém um compartilhamento até que isto seja chamado, de propósito, para que uma tela montada tarde ainda o veja. Chame clearShared() somente DEPOIS que o compartilhamento estiver salvo com segurança - se o salvamento falhar, deixe-o para que o usuário não o perca.

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 de range é 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 web

Abra 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 folder

Faç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 300

Um 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 como appmint-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 screen

Observaçþ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 espere false lá.
  • 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.mediaSession do app, instalado quando a página termina de carregar. Um onAction registrado antes (por exemplo, em um efeito de montagem) é perdido, então registre-o na primeira reprodução.
  • A ação seek (com positionMs) é entregue somente na web; a notificação do Android não tem avanço por posição.
  • artworkUrl precisa ser um endereço https:// completo na internet. No Android, cast(url) abre o seletor "Transmitir para..." (um repasse; resolve true assim 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.navEnd appmintNative#

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

Exemplo

Documentado junto com appmintNative.navStart - o exemplo de lá mostra este também.

navStart() mostra a fina barra de carregamento superior do app para uma mudança de tela que o app não consegue ver sozinho; navEnd() a encerra.

Retorna: nada (ambos são síncronos). Requer: nada. Na web (sem barra do shell do app) ambos não fazem nada.

Use-os apenas para uma espera em que nada no DOM muda até os dados chegarem, e sempre combine com try/finally:

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

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

Envolva um envio que demora um pouco, para que o usuário veja a barra enquanto o servidor trabalha.

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

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

Crie uma pequena função auxiliar que envolva qualquer promise.

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

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

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

Observações: elas chamam WebToApk.navStarted() / navSettled(). O app já controla a barra quando o seu app troca telas no DOM, então a maioria das mudanças de rota não precisa de nada. Nada é desenhado para uma mudança menor que cerca de 200 ms, e uma barra que nunca termina para sozinha após 2,5 segundos. Continue exibindo seu próprio indicador ou skeleton; a barra é um extra.

appmintNative.navStart appmintNative#

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

Exemplo

navStart() mostra a fina barra de carregamento superior do app para uma mudança de tela que o app não consegue ver sozinho; navEnd() a encerra.

Retorna: nada (ambos são síncronos). Requer: nada. Na web (sem barra do shell do app) ambos não fazem nada.

Use-os apenas para uma espera em que nada no DOM muda até os dados chegarem, e sempre combine com try/finally:

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

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

Envolva um envio que demora um pouco, para que o usuário veja a barra enquanto o servidor trabalha.

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

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

Crie uma pequena função auxiliar que envolva qualquer promise.

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

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

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

Observações: elas chamam WebToApk.navStarted() / navSettled(). O app já controla a barra quando o seu app troca telas no DOM, então a maioria das mudanças de rota não precisa de nada. Nada é desenhado para uma mudança menor que cerca de 200 ms, e uma barra que nunca termina para sozinha após 2,5 segundos. Continue exibindo seu próprio indicador ou skeleton; a barra é um extra.

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.onLine mais navigator.connection.type quando 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 / --bg offline, --accent / --accent-ink online). Tem role="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.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 0 lá — 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 como 0).
  • 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 / offline e informa networkStatus().
  • 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: contain em el e restaura ao cancelar a inscrição.

appmintNative.onShared appmintNative#

onShared(cb: (p: { text, subject, url, receivedAt }) => void): () => void

Exemplo

Chama você quando outro app compartilha texto ou um link com este app enquanto ele já está aberto.

Retorna: uma função de cancelamento — chame-a ao desmontar. Requer: a opção Receber compartilhamentos de outros apps da compilação com "Texto e links" — ativada automaticamente quando o seu código chama onShared( ou sharedText(.

Trate os dois casos - um compartilhamento que abriu o app e outro que chega depois:

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

export function useIncomingShares(save: (text: string, url: string) => Promise<void>) {
  useEffect(() => {
    if (can('shareTarget') === 'none') return;
    const handle = async (p: { text: string; url: string }) => {
      await save(p.text, p.url);
      clearShared();
      toast('Added');
    };
    const waiting = sharedText();
    if (waiting) void handle(waiting);                    // shared before the app opened
    return onShared((p) => void handle(p));              // shared while open; returns the unsubscribe
  }, [save]);
}

Uma página escrita à mão pode ouvir o evento do shell por conta própria:

window.addEventListener('appmint:shared', function (e) {
  var p = e.detail;                  // { text, subject, url, receivedAt }
  if (p && p.text) console.log('shared:', p.text);
});

Preencha previamente um formulário de "nova nota" a partir de um link compartilhado, apenas onde a compilação pode receber compartilhamentos:

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

export function NewNote() {
  const [body, setBody] = useState('');
  useEffect(() => {
    if (can('shareTarget') === 'none') return;
    return onShared((p) => {
      setBody([p.subject, p.text, p.url].filter(Boolean).join('\n'));
      toast('Shared text added');
    });
  }, []);
  return <textarea value={body} onChange={(e) => setBody(e.target.value)} />;
}

Observaçþes:

  • Android: escuta o evento appmint:shared do shell; o shell também chama window.onAppMintShared(detail) quando uma página o define. O mesmo payload também é mantido para sharedText() até clearShared().
  • Na web nunca dispara e retorna um cancelamento que não faz nada.

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 origins

Ofereç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. Retorna false em 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 sem video retorna false.
  • 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 BarcodeDetector do 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 formats quando 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() é false no 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 usa manifestUrl(). 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() retorna null para uma publicação pausada ou revertida, um manifesto cuja lista platforms não inclui web, 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 novo announcementId seja 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.body existir (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 com BarcodeDetector em 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ê recebe null.
  • 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 null como 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.scanCode do 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. manualInput deixa 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; apenas value e format são preenchidos ali, e o navegador pede a permissão da câmera.
  • null sempre significa que o usuário fechou o escâner; qualquer outro resultado rejeita com um code, então mostre uma mensagem em catch.
  • Chame a partir de um toque, nunca ao carregar a página. Restrinja formats quando 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 app

Um 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.share appmintNative#

share(input: { title?, text?, url?, files? }): Promise<boolean>

Exemplo

Abre a folha de compartilhamento do sistema no app e a Web Share API em um navegador.

Retorna: Promise<boolean> - true quando a folha abriu; false quando o usuário a dispensou ou o compartilhamento não está disponível. Rejeita quando o próprio compartilhamento é inválido (por exemplo, uma url sem suporte) ou outro compartilhamento ainda está aberto. Requer: nada.

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

export function ShareRecipe({ recipe }: { recipe: { title: string; url: string } }) {
  if (can('share') === 'none') return null;
  return (
    <button
      onClick={async () => {
        const ok = await share({ title: recipe.title, text: 'Try this recipe', url: recipe.url });
        if (ok) toast('Shared');           // false = the user closed the sheet; say nothing
      }}
    >
      Share
    </button>
  );
}

Somente texto (sem link) também funciona:

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

await share({ text: 'My score today: 42' });

A forma no estilo Capacitor Share.share(opts) chama o mesmo código, mas REJEITA quando o usuário cancela (contrato do Capacitor) e resolve { activityType: '' } nos demais casos; Share.canShare() resolve { value: can('share') !== 'none' }:

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

if ((await Share.canShare()).value) {
  try {
    await Share.share({ title: 'Invite', url: 'https://example.com/join' });
  } catch {
    /* cancelled or unavailable — never report success here */
  }
}

Observaçþes:

  • Passe apenas os campos que você tem. No app, texto e links passam pelo plugin Share do Capacitor.
  • files funciona no app Android (os arquivos vão para a janela de compartilhamento como anexos reais; um url é adicionado ao texto) e na web onde o navegador pode compartilhar arquivos (navigator.canShare({ files })). Mantenha os arquivos pequenos — eles são passados ao app em memória.
  • Na web, chame a partir de um toque; os navegadores recusam um compartilhamento que nenhuma ação do usuário iniciou.

appmintNative.Share appmintNative#

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

Exemplo

Documentado junto com appmintNative.share - o exemplo de lá mostra este também.

Abre a folha de compartilhamento do sistema no app e a Web Share API em um navegador.

Retorna: Promise<boolean> - true quando a folha abriu; false quando o usuário a dispensou ou o compartilhamento não está disponível. Rejeita quando o próprio compartilhamento é inválido (por exemplo, uma url sem suporte) ou outro compartilhamento ainda está aberto. Requer: nada.

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

export function ShareRecipe({ recipe }: { recipe: { title: string; url: string } }) {
  if (can('share') === 'none') return null;
  return (
    <button
      onClick={async () => {
        const ok = await share({ title: recipe.title, text: 'Try this recipe', url: recipe.url });
        if (ok) toast('Shared');           // false = the user closed the sheet; say nothing
      }}
    >
      Share
    </button>
  );
}

Somente texto (sem link) também funciona:

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

await share({ text: 'My score today: 42' });

A forma no estilo Capacitor Share.share(opts) chama o mesmo código, mas REJEITA quando o usuário cancela (contrato do Capacitor) e resolve { activityType: '' } nos demais casos; Share.canShare() resolve { value: can('share') !== 'none' }:

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

if ((await Share.canShare()).value) {
  try {
    await Share.share({ title: 'Invite', url: 'https://example.com/join' });
  } catch {
    /* cancelled or unavailable — never report success here */
  }
}

Observaçþes:

  • Passe apenas os campos que você tem. No app, texto e links passam pelo plugin Share do Capacitor.
  • files funciona no app Android (os arquivos vão para a janela de compartilhamento como anexos reais; um url é adicionado ao texto) e na web onde o navegador pode compartilhar arquivos (navigator.canShare({ files })). Mantenha os arquivos pequenos — eles são passados ao app em memória.
  • Na web, chame a partir de um toque; os navegadores recusam um compartilhamento que nenhuma ação do usuário iniciou.

appmintNative.sharedText appmintNative#

sharedText(): { text, subject, url, receivedAt } | null

Exemplo

Entrega o texto ou link que outro app compartilhou PARA este app ("Compartilhar → seu app"), por exemplo, um link compartilhado de um navegador ou do YouTube.

Retorna: de forma síncrona, { text, subject, url, receivedAt } ou null quando nada está esperando. url é o primeiro link http(s) encontrado em text ('' se nenhum); receivedAt é em milissegundos desde 1970. Requer: a opção Receber compartilhamentos de outros apps da compilação com "Texto e links" - ativada automaticamente quando seu código chama sharedText( ou onShared(.

Verifique na montagem, salve e depois limpe:

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

export function useSaveSharedLink(save: (item: { title: string; url: string; text: string }) => Promise<void>) {
  useEffect(() => {
    if (can('shareTarget') === 'none') return;     // on the web
    const p = sharedText();
    if (!p) return;
    void save({ title: p.subject || p.url || 'Shared note', url: p.url, text: p.text })
      .then(() => { clearShared(); toast('Saved'); });
  }, [save]);
}

Escute um compartilhamento que chega com o app já aberto usando onShared e limpe-o depois de usado.

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

export function useIncomingShare() {
  const [text, setText] = useState(() => sharedText()?.text ?? '');
  useEffect(() => onShared((p) => setText(p.text)), []);
  return { text, done: () => { clearShared(); setText(''); } };
}

Preencha previamente um formulário a partir de um link compartilhado e não mostre nada na web, onde sharedText() é null.

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

export function NewBookmark() {
  const [url, setUrl] = useState('');
  useEffect(() => {
    if (can('shareTarget') === 'none') return;   // web: nobody can share into the page
    const p = sharedText();
    if (p?.url) setUrl(p.url);
  }, []);
  return <input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="Paste a link" />;
}

Observaçþes:

  • O compartilhamento é MANTIDO até você chamar clearShared(), então um app que é montado depois do compartilhamento ainda o vê. Limpe depois de salvar, ou a próxima montagem o salvará de novo.
  • Para um compartilhamento que chega com o app aberto, use onShared().
  • Uma foto ou vídeo compartilhado COM uma legenda: a legenda chega aqui, o arquivo pela API de arquivo aberto.
  • Somente no app Android. can('shareTarget') é 'none' na web, e sharedText() retorna null lá.

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óprio close() 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 route sem / 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ão new URL(u).pathname é a sua rota. Leia com launchUrl() ao montar e onDeepLink() 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 = normal

Fale 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 partial ativado (o padrão), onResult(text, false) chega conforme as palavras vêm, e depois uma vez onResult(text, true) com o texto final. Com partial: false você 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, onResult nã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. lang usa 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 icons

Alterne 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-action para você: pan-y para handlers horizontais (a rolagem vertical continua funcionando), pan-x para verticais, none para 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 (--ink sobre --bg), acima da margem da área segura, com role="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 (--ink sobre --bg), acima da margem da área segura, com role="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 a 0 significa que nenhuma cópia está na tela inicial ainda.
  • Campos do cartão: title (vazio = o nome do app), value (exibido grande em accent, 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) e bg / 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 valor hsl()/oklch() volta às cores padrão. set() retorna false para um textSize/align desconhecido ou uma cor de linha inválida.
  • Um toque abre o app com appmint-shortcut://<package><route>; leia com launchUrl() / 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, onde set() e requestPin() retornam false.

Gerado a partir do runtime do app e de seus arquivos de exemplo a cada build da documentação. Leia em Markdown · Todas as famílias.

Conferido com a ponte atual em 22/10/2026.