Crear APK gratis

API del puente de JavaScript

appmintNative (apps creadas con IA)

Los envoltorios tipados que todo proyecto creado con IA importa desde @/lib/appmintNative. Cada uno elige la ruta nativa dentro de la app y una ruta web, o un honesto none, en un navegador: una sola llamada funciona en todas partes.

appmintNative.actionSheet appmintNative#

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

Ejemplo

Muestra una hoja inferior de "elige una de estas": el control nativo para el menú de acciones de una fila.

Devuelve: Promise<number | null> - el índice de la opción elegida, o null cuando el usuario cerró la hoja (toque en el fondo, Escape, cancelar del sistema). Cerrarla es normal, no un error. Necesita: 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); }
}

Ăbrela desde un botĂłn visible y con una pulsaciĂłn larga en la fila:

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

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

La forma de Capacitor ActionSheet.showActions({ title?, options }) acepta las mismas opciones y resuelve { index }, con -1 si se descarta:

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)`

Notas:

  • En la app de Android el plugin ActionSheet de Capacitor dibuja la hoja del sistema; destructive usa su estilo destructivo.
  • En el resto es una hoja real con la paleta de la app (--surface, --ink, --danger), con fondo, Escape, el foco movido a la hoja y una vibración de selección.
  • Una lista options vacía resuelve null al instante. Para cualquier contenido que no sea una lista de acciones, usa sheet().

appmintNative.ActionSheet appmintNative#

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

Ejemplo

Documentado junto con appmintNative.actionSheet: el ejemplo de allí también muestra este.

Muestra una hoja inferior de "elige una de estas": el control nativo para el menú de acciones de una fila.

Devuelve: Promise<number | null> - el índice de la opción elegida, o null cuando el usuario cerró la hoja (toque en el fondo, Escape, cancelar del sistema). Cerrarla es normal, no un error. Necesita: 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); }
}

Ăbrela desde un botĂłn visible y con una pulsaciĂłn larga en la fila:

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

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

La forma de Capacitor ActionSheet.showActions({ title?, options }) acepta las mismas opciones y resuelve { index }, con -1 si se descarta:

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)`

Notas:

  • En la app de Android el plugin ActionSheet de Capacitor dibuja la hoja del sistema; destructive usa su estilo destructivo.
  • En el resto es una hoja real con la paleta de la app (--surface, --ink, --danger), con fondo, Escape, el foco movido a la hoja y una vibración de selección.
  • Una lista options vacía resuelve null al instante. Para cualquier contenido que no sea una lista de acciones, usa sheet().

appmintNative.addContact appmintNative#

addContact(contact: Contact): Promise<boolean>

Ejemplo

Guarda un contacto nuevo en la agenda del telĂŠfono. Solo en la app de Android: el puente del shell lo guarda directamente (no se abre ninguna pantalla de Contactos).

Devuelve: Promise<boolean> - true una vez guardado, false cuando el usuario rechazó el permiso de contactos o falló el guardado. Se rechaza con un Error cuyo code es 'not_enabled' (la compilación no activó la escritura de contactos) o 'foreign_origin', y tras 5 minutos de espera sin respuesta. Requiere: el interruptor Editar contactos en el paso 4 (Acceso) —la compilación lo activa cuando tu código llama a esto— y el permiso del usuario en el primer uso.

Guarda un cliente desde un formulario.

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
  }
}

Omite un contacto que ya estĂĄ guardado. searchContacts lee la agenda, asĂ que comprueba antes de añadir:

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';
}

Distingue un error de compilación de un rechazo. Un rechazo con code 'foreign_origin' o 'not_enabled' es un problema de compilación; false es decisión del usuario:

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>;
}

Notas: Guarda al instante: no se abre ningún editor de contactos, así que confirma antes con el usuario en tu propia pantalla. Guarda los números de teléfono como móviles y el contacto en el dispositivo (sin cuenta). Escribir contactos es sensible: decláralo en tu formulario de seguridad de los datos de Play y en tu política de privacidad.

appmintNative.alarmSupport appmintNative#

alarmSupport() — @/lib/appmintNative

Ejemplo

Si scheduleAlarm puede establecer una alarma ahora mismo y qué falta si no.

Devuelve: 'ready', 'not_enabled' (compilada sin Despertador), 'needs_exact_alarm' (Android 12+: las alarmas exactas están desactivadas), 'needs_notification_permission' (Android 13+) o 'unavailable' (la web e iOS: no dibujes controles de alarma allí). Requiere: 🔔 Despertador en el paso Acceso (paso 4) al 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>;
}

Establece una alarma diaria solo cuando el telĂŠfono estĂĄ listo, y di por quĂŠ cuando no lo estĂĄ:

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.';
}

Oculta la pantalla de alarma en la web y reacciona cuando termina una alarma, combinĂĄndolo con onAlarm:

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

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

appmintNative.App appmintNative#

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

Ejemplo

El nombre de la app, el id del paquete y la versión, una forma de cerrar la app y los eventos de la app, con la forma del plugin App de Capacitor.

Devuelve: getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → la forma de Capacitor (una Promise del handle que tambiĂŠn tiene remove()). Eventos: appStateChange ({ isActive }), pause, resume (la pĂĄgina pasando a / volviendo de segundo plano), appUrlOpen ({ url }, un enlace profundo: igual que onDeepLink), backButton (la pulsaciĂłn o gesto de retroceso; tu controlador decide: la app no se cierra ademĂĄs). Requiere: nada.

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

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

Cierra la app desde un elemento "Salir" que no sea la única forma de continuar:

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();
}

Muestra la versiĂłn de la app en una pantalla de 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>;
}

Notas: En la app getInfo lee el plugin App de Capacitor; cuando falta usa el nombre y la versiĂłn del puente del shell (id y build son entonces ''), y en la web document.title con una versiĂłn vacĂa. exitApp usa el closeWindow del shell en la app y no hace nada en la web, asĂ que nunca hagas de Salir la Ăşnica forma de avanzar. Para eventos de pausa/reanudaciĂłn y enlaces profundos consulta Capacitor.Plugins.App, launchUrl() y onDeepLink().

appmintNative.appLauncher appmintNative#

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

Ejemplo

Abre otra app mediante un enlace - whatsapp://send?text=…, tel:, mailto:, geo:, o un enlace https que el sistema entrega a su app - y pregunta primero si eso puede funcionar.

Devuelve: canOpen() → Promise<boolean>. open() → Promise<boolean> (false cuando ninguna app aceptó el enlace). Requiere: 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);
}

Llamar o enviar un correo (no hace falta comprobar; un teléfono siempre tiene una app para esto):

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

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

Abre la app de mapas con un enlace geo: e indícalo cuando nada lo aceptó:

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.');
}

Notas: En la app usa el plugin AppLauncher de Capacitor. En Android 11+ canOpen solo ve los esquemas que declara la app (https, geo, google.navigation, tel, mailto, sms, whatsapp, intent, upi); para los demás responde false aunque la app esté instalada: llama a open() y comprueba su resultado. En la web no se puede comprobar un esquema personalizado, así que canOpen es true solo para http(s), y open navega al enlace.

appmintNative.appLock appmintNative#

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

Ejemplo

Vuelve a bloquear la app cuando el usuario regresa tras haber estado un rato en segundo plano: pide huella / rostro / PIN (unlock()) y activa la pantalla de privacidad para que el selector de apps no muestre tu contenido.

Devuelve: una función que detiene el bloqueo (llámala al cerrar sesión o al desmontar). onLocked(true) se dispara cuando la app se bloquea, onLocked(false) tras un desbloqueo correcto. Necesita: lo mismo que unlock(): Android: Huella digital marcada al 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;
}

En el componente raíz: const locked = useAppLock(); if (locked) return <LockScreen />;.

Detén el bloqueo al cerrar sesión, para que a un usuario sin sesión no se le pida 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;
}

Ofrece el bloqueo solo donde el dispositivo pueda abrirlo y combínalo con unlock() para un botón manual "Bloquear ahora":

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>
    </>
  );
}

Notas: Si el dispositivo no tiene nada con qué desbloquear (can('unlock') === 'none'), no hace nada y devuelve una función vacía: un bloqueo de app que nunca pudiera abrirse atraparía al usuario. Si el usuario cancela, vuelve a pedirlo hasta cinco veces y luego queda bloqueado (mantén visible tu pantalla de bloqueo) hasta que la app pase a segundo plano y vuelva. Esto es independiente de la opción PIN Lock del asistente de compilación, que la app muestra de forma nativa al iniciar.

appmintNative.appUpdate appmintNative#

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

Ejemplo

Comprueba en Google Play si hay una versión más nueva e inicia la actualización dentro de la app de Play (descarga e instala en el sitio).

Devuelve: check() → Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null en la web, sin servicios de Play o cuando Play falla). updateAvailable es la respuesta propia de la tienda. start() → Promise<boolean>: true solo cuando la actualización de Play se completó, false cuando el usuario canceló, falló o no había ninguna disponible. Requiere: nada; la app debe estar instalada desde 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);
  });
}

Compara el cĂłdigo de versiĂłn de Play con el instalado, y trata null como "no se puede comprobar aquĂ":

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

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

Un banner de React que comprueba una vez cuando se abre la pantalla:

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>;
}

Notas: updateAvailable es la respuesta propia de Play; inPlace añade que Play puede ejecutar ahora mismo su actualización dentro de la app. availableVersion es el CÓDIGO de versión de Play (Play no da nombre): compáralo con currentVersionCode, nunca con currentVersion, que es el nombre de la versión.

appmintNative.askForReview appmintNative#

askForReview(options): Promise<ReviewOutcome>

Ejemplo

Pide a Google Play que muestre su propia hoja de valoración dentro de la app e informa de lo que ocurrió.

Devuelve: Promise<ReviewOutcome> - { result, openedStore, message? } con result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' en la web). { openStore: true } abre la ficha de Play cuando la hoja no se mostrĂł. Requiere: nada; la hoja solo aparece en una app instalada desde 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!');
}

Dile al usuario quĂŠ pasĂł en 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}` : ''}.`;
  }
}

Pregunta una sola vez, después de que el usuario termine su tercera tarea, y recuerda que ya preguntaste:

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();
}

Notas: Pide despuĂŠs de un momento de ĂŠxito y pocas veces. Una instalaciĂłn de prueba fuera de la tienda siempre responde not_from_play.

appmintNative.backgroundLocation appmintNative#

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

Ejemplo

Ubicación que sigue llegando mientras la app está en segundo plano: start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.

Devuelve: start(options) → Promise<BgResult> ({ ok, background, backgroundPermission, precise } o { ok: false, reason }). stop / clearBuffered → Promise<boolean>. buffered() → Promise<LocationFix[]>. requestAlways() → Promise<{ foreground, background, precise, error? }>. onFix(cb) → una cancelación de suscripción; cb recibe cada detalle de appmint:location. Requiere: Ubicación en segundo plano en la compilación; permiso de ubicación (pídelo antes con navigator.geolocation).

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

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

Pide "Permitir todo el tiempo" antes de empezar y explica un rechazo:

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

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

Un hook de React que muestra puntos en vivo y deja de escuchar cuando se cierra la pantalla:

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'

Ejemplo

Peticiones HTTP nativas que se ejecutan con la app cerrada: enqueue (un autoguardado puntual y duradero que se envía cuando hay conexión), schedule (una petición repetitiva, como mínimo cada 15 minutos), cancel y list.

Devuelve: enqueue / schedule → Promise<boolean>; cancel → Promise<void>; list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } - pendingDelivery en Android: un envĂo Ăşnico aĂşn no entregado). Requiere: nada en la app de Android (usa WorkManager). En la web can('backgroundSync') es 'none' y enqueue / schedule se resuelven false.

Autoguardado duradero: el mismo id reemplaza el guardado que aĂşn no se ha 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),
  });
}

SincronizaciĂłn repetida, lista y cancela:

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 sin procesar (lo que espera el envoltorio en 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);
});

Notas: true significa que el trabajo se puso en cola, no que se entregó. false significa que no se puso en cola: el shell respondió status: 'failed' (una URL que no empieza por http, un id vacío, o el sistema la rechazó) o no respondió en 10 segundos. El JavaScript de la página nunca se ejecuta en segundo plano; solo lo hace esta solicitud. Una respuesta 4xx no se reintenta; los errores de red y 5xx se reintentan hasta 8 veces.

appmintNative.backgroundTasks appmintNative#

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

Ejemplo

El propio cĂłdigo de la pĂĄgina, ejecutado periĂłdicamente con la app cerrada (wrapper tipado sobre AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.

Devuelve: register / runNow → Promise<BgResult>; unregister / done → Promise<boolean>; list() → las tareas registradas con lastRunAt / lastResult; isBackgroundRun() / currentRun() son síncronos. Requiere: Ejecutar código de la página con la app cerrada en la compilación.

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();
}

Regístrate al iniciar sesión, elimina al cerrar sesión y enumera lo 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.';
}

Ejecuta una tarea ahora mismo desde un botón "Sincronizar ahora" y comprueba la respuesta:

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'

Ejemplo

Establece o borra el contador de no leídos en el icono de la app en el lanzador.

Devuelve: badge.set(count) → Promise<boolean> (false cuando el plugin Badge no estĂĄ, p. ej. en la web); badge.clear() → Promise<void>. Requiere: nada en Android (usa el plugin Badge integrado).

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();
}

Refleja el contador de no leídos cuando llega un mensaje push con la app abierta:

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);
}

Marca todo como leído y borra el contador a la vez; omite la insignia donde no exista:

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;
}

Notas: en Android el aspecto depende del launcher: muchos muestran un número, otros solo un punto, otros nada; no pongas información solo en la insignia. En la web can('badge') es 'none'.

appmintNative.barcodeFormats appmintNative#

barcodeFormats(): Promise<string[]>

Ejemplo

Lista los formatos de códigos de barras y QR que el BarcodeDetector de este dispositivo puede leer.

Devuelve: Promise<string[]> - nombres de formatos como 'qr_code', 'ean_13', 'code_128'; un arreglo vacío cuando la lectura de códigos de barras no está disponible aquí. Nunca rechaza. Necesita: nada.

Muestra la opción "Escanear código de barras de un producto" solo cuando el dispositivo lee ese formato:

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

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

Limita un escaneo a lo que admite el dispositivo:

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

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

Crea un selector de formatos que liste solo lo que este dispositivo puede leer:

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>
  );
}

Notas:

  • Consulta el BarcodeDetector.getSupportedFormats() del motor del navegador. El WebView de la app de Android lo tiene (medido: aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e); muchos navegadores de escritorio y Safari no lo tienen, y responden [].
  • Esta lista trata de readBarcodeFrom() / scanBarcode(). scanCode() en la app usa el escĂĄner de Google y no depende de ella.

appmintNative.batteryOptimization appmintNative#

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

Ejemplo

La exención de optimización de batería: isIgnoring() y request().

Devuelve: isIgnoring() → booleano. request() → Promise<string>: 'already_ignoring', 'dialog_opened' (opción de compilación "Pedir directamente omitir la optimización de batería"), 'settings_opened', 'unavailable', o 'unsupported' fuera de la app de Android. Requiere: nada para la lista de ajustes.

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;
}

Muestra el estado actual en una fila de ajustes:

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>
  );
}

Pide la exención antes de iniciar una tarea larga en segundo plano, junto con 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'

Ejemplo

Sensores de fitness Bluetooth estándar - frecuencia cardíaca, velocidad/cadencia de ciclismo, potencia, velocidad/cadencia de carrera y batería del sensor - con los números ya procesados, en la app de Android.

Devuelve: cada mĂŠtodo devuelve Promise<void> (setWheelCircumference devuelve void). Todo lo demĂĄs llega como eventos de ventana appmint:ble con detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. Requiere: Bluetooth, que la compilaciĂłn activa por sĂ sola cuando el cĂłdigo usa bleSensors. En un navegador (vista previa web) no hay puente de sensores: startScan() responde con {kind:'error', error:'not_enabled'}, asĂ que gestiona ese error. Para saberlo de antemano, comprueba can('bluetooth') === 'native', no !== 'none': en Chrome can('bluetooth') es 'web' (Web Bluetooth), que bleSensors no 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>
  );
}

Condiciona la pantalla a la app instalada.

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>;
}

Detén el escaneo en el primer sensor y observa la velocidad de pedaleo.

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>;
}

Notas: Los escaneos duran 15 segundos (stopScan() termina uno antes); se conecta un sensor a la vez. Códigos de error: not_enabled, permission_denied, bluetooth_unavailable, bluetooth_off, scan_failed, invalid_address. La velocidad necesita dos paquetes y el tamaño de la rueda (por defecto 2096 mm). Los mismos mensajes también van a window.onAppMintBle; escuchar el evento es más sencillo. Para cualquier otro dispositivo Bluetooth usa navigator.bluetooth.

appmintNative.brightness appmintNative#

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

Ejemplo

Sube el brillo de la pantalla para una tarjeta de embarque o un cĂłdigo QR y luego devuelve el control al sistema. Solo nativo: una pĂĄgina web no puede controlar la retroiluminaciĂłn.

Devuelve: get() → Promise<number | null> (0-1 mientras la app sobrescribe el nivel; -1 cuando la app sigue el nivel del sistema; null en la web o cuando no se puede leer). set(value) → Promise<boolean> (valor limitado a 0-1; false en la web). restore() → Promise<void>. Requiere: 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 />;
}

Recuerda el nivel, sube el brillo y luego devuĂŠlvelo.

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;
}

Mantén la pantalla brillante y despierta mientras se muestra un código.

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]);
}

Notas: Cambia solo la ventana de esta app, no el ajuste de brillo del teléfono, y el cambio termina cuando la app se cierra. restore() establece -1 (seguir el sistema). get() lee el valor forzado de la ventana, así que es -1 hasta que llames a set(); no es el nivel del sistema.

appmintNative.browser appmintNative#

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

Ejemplo

Abre una página en una pestaña de navegador dentro de la app (Chrome Custom Tabs) —para términos, páginas de ayuda o una página de inicio de sesión— y una pestaña nueva en la web.

Devuelve: open() → Promise<boolean> (false cuando no se pudo abrir la pestaña). close() → Promise<void>. onClosed(cb) → una función para cancelar la suscripción; cb se ejecuta cuando el usuario cierra la pestaña (solo en la app). Necesita: 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.'); }
}

Un enlace de tĂŠrminos en un componente de 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>
  );
}

Cierra la pestaña desde el cĂłdigo y actualiza cuando el usuario vuelva.

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
  };
}

Notas: toolbarColor usa por defecto el token --bg de la app. La pestaña es un navegador aparte: no comparte las cookies ni el localStorage de la app. close() cierra la pestaña desde el código en la app; en la web no hace nada. onClosed nunca se dispara en la web.

appmintNative.calendar appmintNative#

calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar

Ejemplo

Añade, lista y elimina eventos en el calendario PROPIO del teléfono del usuario (el proveedor de calendario de Android) y abre la app de calendario en una fecha.

Devuelve: requestPermission() → Promise<'granted' | 'denied'>. createEvent(input) → Promise<string | null> (el id del evento; null cuando el usuario rechazó el acceso al calendario; rechaza cuando el evento en sí no pudo crearse, p. ej. fechas no válidas). listEvents({ from, to }) → Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (tiempos en ms de época). deleteEvent(id) / openCalendar(date?) → Promise<boolean>. Solo createEvent rechaza, y solo por un evento que no pudo crearse. Necesita: activa Calendario en el Paso 4 (Acceso) al compilar; la compilación lo marca por ti cuando ve calendar.createEvent(, calendar.listEvents( o calendar.requestPermission( en el 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);
}

Lista esta semana, elimina, abre:

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());

Añade al calendario solo cuando sea posible escribir.

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;
}

Notas: En la web createEvent descarga un archivo .ics que el usuario abre en su calendario (devuelve su UID), listEvents devuelve [] (can('calendarRead') es 'none'), y deleteEvent / openCalendar devuelven false. Los recordatorios y los eventos repetitivos están disponibles en Capacitor.Plugins.Calendar, no a través de este envoltorio.

appmintNative.camera appmintNative#

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

Ejemplo

Hace una foto con la cámara o elige fotos de la galería: el plugin Camera de Capacitor en la app, un campo de archivo con capture en la web.

Devuelve: take() → Promise<Photo | null> con { dataUrl, mime }; null al cancelar o si se deniega el permiso. pick() → Promise<Photo[]>, vacĂo al cancelar. Requiere: CĂĄmara en el Paso 4 (Acceso): la compilaciĂłn lo activa cuando tu cĂłdigo llama a camera.take.

Toma una foto de perfil con la cĂĄmara 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;
}

Elige una foto de la galería con take, o varias con pick.

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

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

Sube una foto convirtiendo la URL de datos en un Blob.

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

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

Notas: Opciones: source ('camera' por defecto, o 'photos'), quality 0-100 (por defecto 80), direction ('rear' por defecto, o 'front'), allowEditing. Las fotos no se guardan en la galerĂa. Un permiso denegado se resuelve con null / [], nunca lanza error: muestra una pista breve para que el usuario sepa por quĂ© no pasĂł nada. La cĂĄmara es sensible: decláralo en tu formulario de Seguridad de los datos de Play.

appmintNative.can appmintNative#

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

Ejemplo

Te dice dónde se ejecutará realmente una función en la plataforma actual, para que la app solo dibuje controles que funcionan.

Devuelve: una cadena, de forma sĂncrona: 'native' (la implementaciĂłn propia de la app: el puente del shell o un plugin de Capacitor incluido en la app), 'web' (un estĂĄndar web real en este navegador) o 'none' (no disponible aquĂ: no dibujes el control). Requiere: nada.

Oculta un control que no puede funcionar en esta 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>;
}

Cambia la redacciĂłn segĂşn la superficie:

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';

Nombres 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.

Muestra una pista en lugar de un control inactivo y convierte una función en un simple sí/no con has():

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

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

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

Notas:

  • Un plugin de Capacitor incluido en la app siempre responde 'native'; si no, se comprueba el puente del shell y despuĂ©s el estĂĄndar web.
  • toast, actionSheet, dialog, statusBar y swipe tienen una versiĂłn web real, asĂ que no son 'none' en un navegador.
  • contactList, contactAdd, smsSend, smsList, callLog, shortcuts, shareTarget y widget son 'none' en la web.
  • Algunas funciones existen solo cuando la compilación las activó (acceso a carpetas, claves seguras, controles multimedia, SMS, contactos). Pregunta a can(), no isNativeApp().
  • Un nombre desconocido devuelve 'none'. Para un simple sí/no usa has(cap).

appmintNative.clearPushInbox appmintNative#

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

Ejemplo

Vacía la bandeja de AppMint Push en este teléfono. Es permanente: no se puede deshacer.

Devuelve: boolean de forma sĂncrona: true cuando se borrĂł; siempre false en la web. Requiere: Android: AppMint Push activado en el paso Integrar (Paso 3) al compilar.

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

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

Muestra Borrar solo cuando haya algo que borrar e informa del 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>
  );
}

Marca todo como leído en lugar de borrarlo, para no perder 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;
}

Notas: las notificaciones de la bandeja y tu historial de envĂos en AppMint no se tocan; los mensajes nuevos siguen llegando.

appmintNative.clearShared appmintNative#

clearShared(): void

Ejemplo

Olvida el texto o enlace que otra app compartió con esta, una vez que tu app lo haya guardado.

Devuelve: nada. Requiere: nada (no hace nada en la 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;
}

Borra lo compartido solo despuĂŠs de guardarlo, y consĂŠrvalo cuando falle el guardado:

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
  }
}

Gestiona un contenido compartido que llega mientras la app está abierta:

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]);
}

Notas: sharedText() conserva un contenido compartido hasta que se llama a esto, a propósito, para que una pantalla que se monte tarde aún lo vea. Llama a clearShared() solo DESPUÉS de que el contenido esté guardado de forma segura; si el guardado falla, déjalo para que el usuario no lo pierda.

appmintNative.click appmintNative#

click() — @/lib/appmintNative

Ejemplo

Reproduce una vez el sonido al tocar de la app, para tus propios botones.

Devuelve: void. Requiere: Sonido al tocar activado en el paso 3 (Integrar) al compilar. En la web, y en apps compiladas sin Sonido al tocar, no hace 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>
  );
}

Deja que el usuario apague el sonido en tus propios ajustes:

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
}

ReprodĂşcelo desde una fila de lista junto con un toast:

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

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

Notas: Con Seguir el ajuste del teléfono (recomendado) el clic es silencioso cuando el usuario desactivó los "Sonidos al tocar" del teléfono. Con Reproducir siempre en cada toque, cada toque ya hace clic, así que no hacen falta llamadas extra a click(). Solo Android (el del shell WebToApk.playClick).

appmintNative.Clipboard appmintNative#

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

Ejemplo

El portapapeles con la forma del plugin Clipboard de Capacitor: el mismo cĂłdigo que copy() / paste().

Devuelve: write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value es '' cuando el portapapeles estĂĄ vacĂo). Requiere: 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
  }
}

Copia un cĂłdigo y confirma con un toast:

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

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

Escribe un enlace cuando no hay texto y trata un portapapeles vacío como nada que pegar:

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
  }
}

Notas: En la app usa el plugin Clipboard de Capacitor; en un navegador navigator.clipboard. write envĂa solo string (o url cuando string estĂĄ vacĂo): sin imĂĄgenes. En un navegador, read() puede lanzar un error cuando el usuario deniega el acceso; en la app un portapapeles vacĂo es '', no un error. Consulta can('clipboardRead') antes de mostrar un control de Pegar.

appmintNative.collapsingHeader appmintNative#

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

Ejemplo

Hace que un encabezado de tĂtulo grande reaccione al desplazamiento como los nativos, publicando un valor de 0 → 1 que tu CSS puede usar.

Devuelve: una funciĂłn para cancelar la suscripciĂłn que tambiĂŠn elimina la variable y el atributo. Requiere: nada.

Establece --collapse (de 0 arriba a 1 tras range px de desplazamiento) en el encabezado, y añade data-collapsed cuando estĂĄ totalmente colapsado:

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); }

Deja de escuchar desde un script simple; esto también elimina --collapse y 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);

Notas:

  • El valor por defecto de scroller es la ventana (una pĂĄgina que se desplaza entera); el de range es 64 px.
  • El listener de scroll es pasivo y se actualiza una vez por fotograma de animación, así que no ralentiza el desplazamiento.

appmintNative.composeSms appmintNative#

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

Ejemplo

Abre la app de mensajes con el nĂşmero y el texto rellenos; el usuario pulsa enviar. Funciona en la app de Android (puente del shell) y en la web (un enlace sms:).

Devuelve: boolean de inmediato: true cuando se pidiĂł abrir la app de mensajes. Requiere: ningĂşn permiso.

Un botón "Escribir a la tienda".

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

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

Un hook reutilizable que oculta el botĂłn cuando no hay 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}.`),
  };
}

Invita a un amigo; el usuario pulsa 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');
}

Notas: Prefiérelo a sendSms() para todo lo que el usuario escribe o aprueba: nada sale del teléfono sin que el usuario pulse enviar. En la web navega a sms:…?body=…. Una llamada a composeSms( no activa SMS (leer y enviar) al compilar, así que no añade ningún permiso restringido de Play.

appmintNative.copy appmintNative#

copy(text: string): Promise<boolean>

Ejemplo

Coloca texto en el portapapeles del sistema.

Devuelve: Promise<boolean> - true cuando se copió el texto, false cuando el portapapeles no está disponible o el navegador lo rechazó. Requiere: 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>
  );
}

La forma de Capacitor hace lo mismo:

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

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

Informa de ambos resultados, con una respuesta táctil solo en caso de éxito:

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;
}

Notas: En la app, el plugin Clipboard de Capacitor escribe el texto (un error del plugin rechaza la promesa en lugar de devolver false). En un navegador usa navigator.clipboard.writeText, que necesita una página https y normalmente un toque del usuario.

appmintNative.copyImage appmintNative#

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

Ejemplo

Copia una imagen al portapapeles para pegarla en apps de chat y correo: el portapapeles de la app en la app de Android, la API Async Clipboard en la web.

Devuelve: Promise<boolean> - true una vez copiado. Requiere: nada; condiciona el control a can('clipboardImage').

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

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

Copia una foto de la cámara y luego confírmalo con un toast.

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

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

Muestra el botón solo donde el portapapeles acepta imágenes y copia una imagen desde una 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>
  );
}

Notas: PNG es el tipo más seguro en todas partes; la app de Android también acepta JPEG y WebP.

appmintNative.Device appmintNative#

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

Ejemplo

La forma de @capacitor/device: modelo del telĂŠfono, versiĂłn del sistema, un ID estable y el idioma. Usa el plugin Device de Capacitor en la app y la respuesta del navegador en la web.

Devuelve: getInfo() → Promise<DeviceInfo & { operatingSystem, isVirtual }> (con Capacitor tambiĂŠn manufacturer, webViewVersion, â¦); getId() → Promise<{ identifier }>; getLanguageCode() → Promise<{ value }> (dos letras, p. ej. "en"). Requiere: nada.

Muestra el modelo y la versión del sistema operativo en una pantalla Acerca de.

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

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

Un ID estable y el código de 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 }),
});

Muestra la versión de la app y el modelo del dispositivo en el pie de los ajustes. El informe de sincronización da la versión de la app; el plugin añade el sistema operativo.

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

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

Notas: En la app el deviceInfo() de sincronización también rellena model y osVersion; Device.getInfo() añade operatingSystem, isVirtual y los campos extra del plugin. En la web operatingSystem es 'unknown' y model está vacío. getId() en la web es el ID de localStorage procedente de installId() y lanza un error cuando el almacenamiento está bloqueado.

appmintNative.deviceInfo appmintNative#

deviceInfo(): DeviceInfo

Ejemplo

Te dice de forma síncrona dónde se ejecuta la app (platform) y el idioma, además del informe completo del dispositivo del shell de Android dentro de la app de Android.

Devuelve: DeviceInfo al instante: siempre { platform, language } - platform es 'android' en la app y 'web' en un navegador. En la app de Android también rellena model, manufacturer, osVersion (p. ej. "14") y appVersion (p. ej. "1.4.0"), y lleva todas las claves de WebToApk.getDeviceInfo(): android, hardware, screen, app, runtime, installId. En la web añade osVersion (el user agent). Requiere: nada.

Bifurca segĂşn la plataforma durante el renderizado.

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

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

Lee los detalles de Android. Los campos comunes estĂĄn arriba; el resto del informe del shell permanece en sus grupos anidados.

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}%`);
}

Elige el control adecuado según la 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>;
}

Notas: model y appVersion se rellenan solo en la app. En la web osVersion es la cadena user-agent del navegador, no una versión del sistema operativo. Lanza una excepción si la respuesta del shell no es JSON válido (un defecto real, nunca esperado).

appmintNative.Dialog appmintNative#

Dialog.alert / Dialog.confirm / Dialog.prompt

Ejemplo

DiĂĄlogos reales del sistema: un mensaje, una pregunta de sĂ/no, una pregunta de texto de una lĂnea. La app usa el plugin Dialog de Capacitor; la web usa el propio del navegador alert / confirm / prompt.

Devuelve: alert({ title?, message }) → Promise<void>. confirm({ title?, message, okButtonTitle?, cancelButtonTitle? }) → Promise<{ value: boolean }>. prompt({ title?, message, inputPlaceholder?, inputText? }) → Promise<{ value: string; cancelled: boolean }>. confirm y prompt nunca rechazan (cerrar es una respuesta); alert rechaza solo cuando no puede mostrarse (sin mensaje). Necesita: 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 y 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());

Confirma antes de cerrar sesión. Cerrar el diálogo sin responder cuenta como "no".

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

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

Notas: Descartar un confirm da { value: false }; descartar un prompt da { value: '', cancelled: true }. Los diálogos bloquean la pantalla: úsalos para decisiones reales, y toast() para mensajes tipo "Guardado". prompt no pasa títulos de botón personalizados al plugin.

appmintNative.dismissPushMessage appmintNative#

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

Ejemplo

Oculta un mensaje de la bandeja de AppMint Push (deslizar para quitar). Deshazlo con restorePushMessage(id).

Devuelve: boolean de forma síncrona - true si estaba oculto. Siempre false en la web. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al 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());
  });
}

Muestra la bandeja de entrada con un gestor de deslizamiento y un estado de deshacer, para que un deslizamiento nunca sea 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>
  );
}

Oculta todos los mensajes leídos a la vez e informa de cuántos se ocultaron.

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;
}

Notas: solo cambia la bandeja de entrada de este teléfono; la notificación de la bandeja y tu historial de envíos se mantienen.

appmintNative.docScanner appmintNative#

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

Ejemplo

Abre el escáner de documentos del sistema: detección de bordes, enderezado, varias páginas; el escáner ML Kit de Google en la app de Android. Las páginas vuelven como URL de datos JPEG.

Devuelve: Promise<{ pages: string[]; pdfPath?: string } | null> - null cuando el usuario cancela, el escáner no puede iniciarse o en la web. isSupported() → Promise<boolean>: si este teléfono puede escanear (Google Play services); false en la web. Necesita: nada que activar (en Android el escáner se ejecuta en Google Play services y no necesita permiso de cámara).

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>
  );
}

Pregunta primero al teléfono, porque un dispositivo antiguo puede no tener el escáner, y luego conserva las 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} />)}
    </>
  );
}

Escanea y luego sube la primera pĂĄgina con el helper de transferencia de archivos.

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;
}

Notas: pageLimit 0 (por defecto) = sin lĂmite; quality 0.1-1 (por defecto 0.85). Con pdf: true, pdfPath es una ruta de archivo en la cachĂŠ de la app, no una URL que la pĂĄgina pueda cargar. Las URL de datos son grandes: redimensiĂłnalas o sĂşbelas en lugar de guardar muchas en localStorage. Para una foto normal usa camera.take().

appmintNative.enableNotifications appmintNative#

enableNotifications(): Promise<EnableNotificationsResult>

Ejemplo

Recupera a un usuario que denegĂł u omitiĂł las notificaciones, desde un botĂłn que ĂŠl pulsĂł: el diĂĄlogo de permiso mientras aĂşn pueda mostrarse y, si no, (app de Android) los ajustes de notificaciones de esta app.

Devuelve: Promise<{ state, changed, openedSettings }> - state es 'granted' | 'prompt' | 'blocked' | 'unavailable', changed es true cuando difiere del estado previo a la llamada, openedSettings es true cuando se usĂł la pantalla de ajustes. Nunca se rechaza. En la web pide a travĂŠs de Notification.requestPermission(). Requiere: notificaciones en la compilaciĂłn (estĂĄn activadas siempre que la app envĂa o recibe notificaciones).

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';
  });
}

Notas: muestra el banner solo para 'prompt' y 'blocked'; ocúltalo para 'granted' y 'unavailable'. Pregunta desde un toque, nunca al iniciar.

appmintNative.fileTransfer appmintNative#

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

Ejemplo

Descarga o sube un archivo con una función de progreso. En la app la transferencia se ejecuta de forma nativa (Capacitor FileTransfer); en la web usa fetch / XMLHttpRequest con la misma función.

Devuelve: download(url, fileName, opts?) → Promise<{ path?: string; blob?: Blob }> - path (un archivo en la caché de la app) en la app, blob en la web (donde el archivo también se guarda a través del navegador). upload(url, file, opts?) → Promise<{ responseCode: number; responseBody: string }>. upload se resuelve con el estado HTTP que respondió el servidor (comprueba responseCode), tanto en la app como en la web; ambos se rechazan cuando falla la transferencia en sí (sin conexión, archivo no válido). Requiere: nada que activar.

Descarga con barra de progreso:

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)}`);
  }
}

Sube un File desde un <input type="file">:

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

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

Descarga con un estado de progreso y luego abre el archivo, todo en un solo hook.

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

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

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

Notas: fraction es 0 mientras el servidor no envía la longitud. En la app una descarga queda en la carpeta CACHÉ de la app (no en Descargas del usuario); para dar una copia al usuario, usa saveFile. En una subida la app primero copia el archivo en su caché, así que los archivos muy grandes necesitan ese espacio libre. method (predeterminado 'POST') y fileName son opciones de subida opcionales.

appmintNative.fileViewer appmintNative#

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

Ejemplo

Abre un documento (PDF, imagen, archivo de ofimĂĄtica) en la app visor del telĂ©fono. En la web abre la direcciĂłn en una pestaña nueva.

Devuelve: Promise<boolean> - true cuando se abrió el visor, false cuando falló (no hay app para ese tipo, ruta incorrecta) o el destino no se puede abrir en esta plataforma. Requiere: nada que activar; usa el plugin FileViewer de Capacitor que viene incluido en la app.

Abre un documento desde la 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.');
}

Abre un archivo que descargaste antes con fileTransfer.download (una ruta local en la 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

Abre un enlace desde un botón e informa al usuario cuando ningún visor pudo abrirlo.

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>
  );
}

Notas: Un destino http(s) usa openDocumentFromUrl; cualquier otro se trata como una ruta local (se quita file://). En la web solo se abren destinos http(s):, blob: y data:; una ruta local responde false.

appmintNative.folder appmintNative#

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

Ejemplo

Una carpeta completa que el usuario autoriza una vez y la app conserva entre inicios, con operaciones reales de archivos dentro (en la app: el puente de carpetas del shell).

Devuelve: todos los miembros son asĂncronos: connected() → boolean, label() → string, request() → boolean (false al cancelar), disconnect() → void, list(path) → [{ name, uri, isDirectory, length, lastModified }], readText(path) → string | null (null = no existe el archivo), writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean, stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. Requiere: una app creada con IA activa por sĂ sola el interruptor Acceso nativo a carpetas (SAF) cuando el cĂłdigo usa folder; en una compilaciĂłn manual mĂĄrcalo en el Paso 4 (Acceso).

Conecta, muestra la carpeta y desconecta:

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();
}

Lee, escribe, lista, renombra y elimina: cada ruta es relativa a la carpeta concedida ('' es su raĂz):

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

Haz una copia de una nota en la carpeta autorizada y di con claridad cuando no haya ninguna carpeta 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');
}

Notas: can('folderAccess') es 'none' en la web: no existe un equivalente de carpeta persistente en un navegador de teléfono, así que no muestres la función allí (usa pickFile() / saveFile() para archivos sueltos). Llama a request() solo desde un toque del usuario. .. y las rutas absolutas se rechazan. La propia carpeta autorizada nunca puede eliminarse. stat() funciona con archivos y subcarpetas (una carpeta tiene isDirectory: true y size: 0); stat('') —la raíz— es null, usa label() para ella. request() se puede volver a llamar tras cancelar.

appmintNative.fullscreen appmintNative#

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

Ejemplo

Oculta las barras del sistema (en la app) o entra en pantalla completa del navegador (en la web), para un vídeo, un juego o una vista de lectura.

Devuelve: enter() y exit() devuelven Promise<void> y nunca se rechazan; isOn() devuelve boolean de forma síncrona. Requiere: 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>
  );
}

Sal de la pantalla completa cuando se cierre la vista:

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

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

Oculta las barras del sistema en una vista de lectura y sigue el estado desde un botĂłn.

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 };
}

Notas:

  • En la app es la pantalla completa propia del shell (las barras de estado y navegación se ocultan); siempre está disponible para la página.
  • En la web la API Fullscreen solo funciona desde un toque del usuario. Una solicitud rechazada se ignora en silencio, asĂ que lee isOn() despuĂ©s en lugar de suponer.

appmintNative.geofences appmintNative#

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

Ejemplo

Geovallas que se disparan con la app cerrada y sobreviven a los reinicios: add, remove, list, events, clearEvents, onEvent, available.

Devuelve: add(spec) → Promise<BgResult> resuelta cuando Play services la aceptó o rechazó ({ id, ok, reason? } - background_permission_required, location_off, too_many_geofences, …). remove / clearEvents → Promise<boolean>. list() → Promise<GeofenceSpec[]>. events() → Promise<GeofenceEvent[]> (los últimos 100, incluidos los ocurridos con la app cerrada). onEvent(cb) → una función para cancelar la suscripción. Necesita: Geofences en la compilación y "Permitir todo el tiempo" (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)));

Enumera las geovallas activas y las transiciones registradas mientras la app estaba cerrada.

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()}`),
  ];
}

Añade una zona de trabajo para entrada y salida, responde a un rechazo según su motivo y elimínala más tarde.

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

Ejemplo

La forma @capacitor/geolocation: el plugin Geolocation de Capacitor en la app, el navigator.geolocation estándar en la web. Todos devuelven el objeto de posición del W3C.

Devuelve: getCurrentPosition(opts) → Promise<GeolocationPosition> (se rechaza si se deniega, por tiempo agotado o sin ubicación); watchPosition(opts, cb) → Promise<string> (el id de la observación); clearWatch({ id }) → Promise<void>; checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (cada 'granted' | 'denied' | 'prompt'; en Android 12+ un usuario que elige "Aproximada" concede solo coarseLocation). Requiere: GPS / Ubicación en el paso 4 (Acceso): la compilación activa ambos cuando tu código llama a getCurrentPosition o watchPosition.

Dónde estoy, una 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?';
  }
}

Sigue una ruta y luego detente. El callback recibe null y un error cuando falla una posiciĂłn.

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; }
}

Obtén una posición puntual. Devuelve null cuando el usuario la rechaza.

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
  }
}

Notas: navigator.geolocation normal también funciona en la app; usa esto cuando quieras la forma de Capacitor. En Android 12+ el usuario puede permitir solo la ubicación aproximada: aun así obtienes una posición, solo que menos precisa. En la web requestPermissions() hace una solicitud real de posición para activar el aviso del navegador. La ubicación son datos personales: decláralo en tu formulario de seguridad de los datos de Play.

appmintNative.getNotificationState appmintNative#

getNotificationState(): Promise<NotificationState>

Ejemplo

Si las notificaciones están activadas y, si no, si la app aún puede pedirlas o solo los Ajustes pueden resolverlo.

Devuelve: Promise<'granted' | 'prompt' | 'blocked' | 'unavailable'>. En la web, Notification.permission asignado igual (default → 'prompt', denied → 'blocked'); 'unavailable' donde no hay notificaciones en absoluto. Necesita: nada.

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

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

Notas: comprueba de nuevo cuando la app vuelva a primer plano (visibilitychange); el usuario pudo haberlo cambiado en Ajustes.

appmintNative.getPushTags appmintNative#

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

Ejemplo

Lee como objeto las etiquetas de AppMint Push de esta instalaciĂłn.

Devuelve: Record<string, string> de forma síncrona, p. ej. { plan: 'pro', cart: 'open' }; {} cuando no hay ninguna y en la web. Necesita: Android: AppMint Push activado en el paso Integrar (Paso 3) al 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');

Establece varias etiquetas en un solo cambio con setPushTags cuando una pantalla cambia más de una, y vacía un valor para quitar su etiqueta:

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)');
}

Lee las etiquetas para mostrar una pista solo cuando esta instalación tiene una etiqueta:

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>;
}

Notas: esto es lo que guarda el telĂ©fono; el centro de envĂos lo ve despuĂ©s de la siguiente conexiĂłn de la instalaciĂłn.

appmintNative.getPushUser appmintNative#

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

Ejemplo

Lee el id de usuario que esta instalación dio por última vez a AppMint Push con setPushUser.

Devuelve: string de forma síncrona - el id guardado, o '' cuando no hay sesión, nunca se estableció o en la web. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al 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);
}

Llama a setPushUser con el id de cuenta tras iniciar sesiĂłn y con '' al cerrar sesiĂłn; devuelve false cuando se rechaza el id o AppMint Push estĂĄ desactivado:

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('');
}

Lee el id guardado en un hook de React, junto con una etiqueta que pertenece a ese usuario:

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;
}

Notas: esto es lo que guarda el teléfono; el centro de envíos ve un cambio tras la siguiente comprobación de la instalación, unos segundos después si hay conexión.

appmintNative.getVolume appmintNative#

getVolume(stream) — @/lib/appmintNative

Ejemplo

Lee el nivel de volumen propio del teléfono para un flujo: 'media', 'alarm', 'ring', 'notification' o 'call'.

Devuelve: { stream, current, max, percent, muted? } (current/max en los pasos propios del teléfono, percent 0-100), o null en la web. Requiere: 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 en la web, así que dibuja el nivel solo cuando sea un 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" />;
}

Sigue las teclas físicas con onVolumeChange y sube un nivel bajo con setVolume:

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

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

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

appmintNative.haptic appmintNative#

haptic(kind?) — @/lib/appmintNative

Ejemplo

Una vibraciĂłn breve y de sensaciĂłn nativa para toques y resultados. LlĂĄmala en cada toque significativo.

Devuelve: void (se lanza y se olvida). kind: 'light' (predeterminado), 'medium', 'heavy', 'selection', 'success', 'warning', 'error'. Requiere: Vibrar en el paso 4 (Acceso); las compilaciones con IA lo activan por ti cuando el código llama a 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 pestañas, interruptores y selectores usa 'selection'; para confirmar un borrado usa 'heavy':

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

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

Un botón de me gusta que vibra suave al activarse y más firme al desactivarse:

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>
  );
}

Notas: En la app usa el plugin Haptics de Capacitor (impact, notification, selectionChanged). En la web usa navigator.vibrate donde el navegador lo tiene, y no hace nada en otros casos; nunca lanza error. Para ritmos mĂĄs largos usa hapticPlay.

appmintNative.hapticPlay appmintNative#

hapticPlay(pattern) — @/lib/appmintNative

Ejemplo

Reproduce un patrĂłn de vibraciĂłn rico para un momento especial (una victoria, una racha, un latido). Usa haptic() para los toques normales.

Devuelve: Promise<boolean>: true cuando se reprodujo el patrón, false cuando no se pudo. Requiere: Vibrar en el paso 4 (Acceso).

Patrones con nombre: '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();
}

Tu propio patrĂłn: una 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
]);

Combínalo con haptic(): una respuesta táctil normal para cada respuesta, el patrón largo solo para el 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
  }
}

Notas: La app usa el plugin de Capacitor AppwrightHaptics (intensidad por pulso donde el telĂŠfono lo permite). La web solo recibe el ritmo. sharpness se acepta pero no tiene efecto en Android. haptic.play(pattern) es la misma funciĂłn.

appmintNative.Haptics appmintNative#

Haptics — @/lib/appmintNative

Ejemplo

El objeto Haptics al estilo Capacitor: los mismos nombres y opciones que @capacitor/haptics, ejecutĂĄndose con el mismo cĂłdigo que haptic().

Devuelve: todos los métodos devuelven void (no una Promesa; await sobre él es inofensivo). Necesita: Vibrar en el Paso 4 (Acceso); las compilaciones con IA lo activan por ti cuando el 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

Un selector que marca cada cambio 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>
  );
}

Un formulario que confirma con una vibraciĂłn de notificaciĂłn y añade una vibraciĂłn corta cuando un campo es incorrecto:

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

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

Notas: selectionStart y selectionChanged dan ambos un toque de selección; selectionEnd no hace nada. Funciona en la app (plugin Haptics de Capacitor); en la web usa navigator.vibrate si está disponible y, si no, no hace nada.

appmintNative.has appmintNative#

has(cap: Capability): boolean

Ejemplo

La forma sĂ/no de can(): true cuando la funciĂłn harĂĄ algo real en esta superficie.

Devuelve: boolean, de forma síncrona - exactamente can(cap) !== 'none'. Necesita: 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>
  );
}

Controla el acceso a una función completa:

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;
}

Elige una etiqueta según dónde se ejecuta: has() decide si dibujar el control, can() te dice de qué tipo es:

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';
}

Notas: Acepta los mismos nombres de capacidad que can() (consulta esa entrada para la lista completa). Usa can() cuando necesites saber DÓNDE se ejecuta ('native' o 'web'), por ejemplo para cambiar una etiqueta.

appmintNative.hideKeyboard appmintNative#

hideKeyboard(): void

Ejemplo

Cierra el teclado en pantalla, por ejemplo cuando se envía un formulario o se abre una hoja sobre el campo.

Devuelve: nada. Requiere: 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>
  );
}

Cierra el teclado cuando se abra una hoja sobre el campo, para que la hoja no quede oculta detrás:

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),
  };
}

CiĂŠrralo cuando el usuario toque en cualquier lugar que no sea un 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>
  );
}

Notas: En la app pide al plugin Keyboard de Capacitor que oculte el teclado. En la web quita el foco del campo activo, que es lo que cierra el teclado en un navegador. Es seguro llamarlo cuando no hay ningĂşn teclado abierto.

appmintNative.inAppCampaigns appmintNative#

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

Ejemplo

Los mensajes dentro de la app que esta instalaciĂłn tiene ahora mismo, como {id, name}[]: las campañas del creador a cuya audiencia pertenece este usuario.

Devuelve: { id: string; name: string }[] de forma sĂncrona; [] en la web y sin AppMint Push. Requiere: Android: AppMint Push activado en el paso Integrar (Paso 3) al compilar.

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

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

Una lista de depuración en una pantalla de administración:

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>;
}

Omite tu propia pista cuando el creador ya ejecuta un mensaje con ese nombre:

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

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

appmintNative.installId appmintNative#

installId(): string

Ejemplo

Da un ID aleatorio estable para esta instalación: el ID propio del shell en la app de Android, un ID aleatorio guardado en localStorage en la web.

Devuelve: string al instante. Necesita: nada.

Asocia una prueba gratuita al dispositivo.

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

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

Cuenta esta instalaciĂłn una sola vez en tu propia analĂtica.

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');
}

Muestra un cĂłdigo de soporte corto que puedas consultar mĂĄs tarde, con un botĂłn 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>;
}

Notas: Se mantiene igual entre actualizaciones de la app y cambia al desinstalar o al "borrar datos". No es un ID de hardware. En la web lanza un error cuando localStorage está bloqueado, a propósito: un ID en blanco compartido mezclaría a los usuarios.

appmintNative.isAlarmRinging appmintNative#

isAlarmRinging() — @/lib/appmintNative

Ejemplo

Si una de las alarmas de la app estĂĄ sonando ahora mismo.

Devuelve: boolean, de forma síncrona; siempre false en la web. Requiere: 🔔 Despertador en el paso Acceso (paso 4) al 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;
}

Muestra los botones Detener y Posponer mientras suena una alarma:

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>
  );
}

Comprueba primero la compatibilidad, para que la página dibuje controles de alarma solo donde una alarma pueda sonar:

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

Ejemplo

Te dice si el cĂłdigo se ejecuta dentro de la app de Android instalada (el puente del shell window.WebToApk estĂĄ presente).

Devuelve: boolean al instante. false en un navegador. Necesita: nada.

Muestra una insignia de "app para Android" y un enlace de descarga en el navegador.

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

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

Etiqueta tus eventos de analĂtica con su origen.

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

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

Oculta el banner "Descarga la app" dentro de la 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>;
}

Notas: platform() da la misma respuesta como cadena ('android' en la app, 'web' en un navegador). Para decidir si se puede mostrar una función, usa can('<capability>') en su lugar: sabe qué plataforma tiene qué función.

appmintNative.keepAlive appmintNative#

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

Ejemplo

Mantiene la app en ejecución en segundo plano con una notificación de servicio en primer plano (start, update, stop, isRunning, onState, available).

Devuelve: start / update → Promise<BgResult> ({ ok, reason?, type?, notificationShown? }; reason: 'unsupported' en un navegador o en iOS, 'not_enabled' cuando la compilación no marcó Mantener en ejecución en segundo plano). stop → Promise<boolean>. isRunning() → booleano. onState(cb) → una función para cancelar la suscripción; cb recibe el detalle de appmint:background-service. Requiere: Mantener en ejecución en segundo plano en la compilación.

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();

Detén el servicio y lee por qué se rechazó start:

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.');
  }
}

Mantén un botón de React sincronizado con el servicio y muestra el progreso en la notificación:

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>

Ejemplo

Mantiene la pantalla encendida mientras se muestra una receta, una ruta, un cĂłdigo QR o un temporizador, y la deja volver a dormirse.

Devuelve: Promise<boolean> - true cuando la solicitud surtiĂł efecto, false cuando nada pudo mantener la pantalla encendida (o el navegador lo rechazĂł). Requiere: 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 />;
}

Mantén la pantalla encendida solo mientras un temporizador está en marcha e informa al usuario cuando no se pudo.

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]);
}

Comprueba primero la capacidad y vuelve a pedir el bloqueo cuando la pestaña vuelva al primer plano (la web lo libera cuando la pĂĄgina queda 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);
  };
}

Notas: En la app usa el plugin KeepAwake (el indicador de mantener la pantalla encendida de la ventana); en la web, la Screen Wake Lock API, que los navegadores liberan por sí solos cuando la pestaña se oculta: llama a keepAwake(true) de nuevo cuando la página vuelva a ser visible si la pantalla debe seguir encendida. Solo funciona mientras la app está en primer plano.

appmintNative.Keyboard appmintNative#

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

Ejemplo

El teclado de software con la forma del plugin Keyboard de Capacitor: ciérralo y entérate cuando cambie el espacio que cubre.

Devuelve: hide() → Promise<void>. addListener(event, handler) → la forma de Capacitor: una Promise del manejador que además tiene remove() por sí mismo, así que await, .then(h => h.remove()) y .remove() funcionan todos. event es 'keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide' o 'keyboardDidHide'; el manejador recibe { keyboardHeight }. Requiere: nada.

Cierra el teclado cuando se envía un formulario o se abre una hoja sobre el campo:

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

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

Escuchar (elimina el handle al 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(); };
}, []);

Combina ambos para desplazar el campo enfocado a la vista cuando se abre el teclado y luego cerrar el teclado con Intro:

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); };
  }, []);
}

Notas: keyboardHeight es la parte del teclado que la pĂĄgina aĂşn debe despejar, no la altura bruta: en la app de Android el shell redimensiona la pĂĄgina para el teclado, asĂ que allĂ vale 0 mientras el teclado estĂĄ abierto (el evento show se sigue disparando, y de nuevo cuando cambia el nĂşmero). Los eventos de ocultar se disparan solo cuando el teclado realmente se cierra, asĂ que pueden seguir "si el teclado estĂĄ abierto". Los eventos "will" y "did" se disparan juntos. Solo para el relleno, onKeyboard((px) => …) es mĂĄs sencillo.

appmintNative.launchUrl appmintNative#

launchUrl(): Promise<string>

Ejemplo

Da el enlace que lanzĂł o reanudĂł la app (un enlace profundo, un atajo de icono, un toque en un widget, una notificaciĂłn) para que la app pueda abrir la pantalla correcta.

Devuelve: Promise<string> - la URL completa, o '' cuando la app se abrió con normalidad. Siempre '' en la web, donde la barra de direcciones ya es la ruta. Requiere: nada para los accesos directos y los toques en widgets; los enlaces externos necesitan la opción Enlaces profundos al compilar.

Enruta según él una vez cuando la app se monte:

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]);
}

Combínalo con onDeepLink() para los enlaces que llegan cuando la app ya está abierta:

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

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

Trata la cadena vacĂa como "abierta normalmente" y elige la pantalla de inicio a partir del enlace solo cuando lo haya. Un atajo como appmint-shortcut://<package>/orders lleva la ruta en su path.

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

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

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

Notas:

  • El valor se CONSERVA, no se consume: un componente que se monta tarde aĂşn lo ve. Un enlace mĂĄs nuevo lo reemplaza.
  • Se lee del shell (WebToApk.getLaunchUrl()); nunca llames a eso directamente en una app creada con IA. Los atajos y toques en widgets llegan como appmint-shortcut://<package><route>.
  • Analiza con new URL(u) y navega a su ruta; no cargues la URL en sĂ.

appmintNative.listCallLog appmintNative#

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

Ejemplo

Lee el historial de llamadas, de mĂĄs reciente a mĂĄs antiguo. Solo en la app de Android.

Devuelve: Promise<CallLogEntry[]> - cada { number, name, type, date, duration }; type es incoming, outgoing, missed, voicemail, rejected, blocked o other, date son milisegundos epoch, duration son segundos. VacĂo cuando el registro de llamadas estĂĄ desactivado o se deniega el permiso. Requiere: el interruptor Registro de llamadas (lectura) en el Paso 4 (Acceso) (la compilaciĂłn lo activa cuando tu cĂłdigo llama a esto) y el permiso del usuario en el primer uso.

Llamadas perdidas de los Ăşltimos 7 dĂas.

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
  }
}

Tiempo total de conversación por contacto a partir de llamadas salientes y entrantes, de mayor a 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]);
}

Una pantalla que explica la lista vacĂa, ya que "sin llamadas" y "permiso denegado" se ven igual.

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 };
}

Notas: limit la app lo limita a 2000. Una lista vacía no distingue "sin llamadas" de "permiso rechazado". can('callLog') es 'none' en la web. El registro de llamadas es un permiso restringido de Google Play: necesitas una Declaración de permisos aprobada en Play Console, o la app será rechazada.

appmintNative.listContacts appmintNative#

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

Ejemplo

Lee la agenda de contactos, una página cada vez. Solo en la app de Android, a través del puente del shell.

Devuelve: Promise<Contact[]>. Un arreglo vacío cuando no hay contactos, cuando se rechaza el acceso o cuando Contactos no se activó al compilar. Necesita: el interruptor Contactos en el Paso 4 (Acceso) - la compilación lo activa cuando tu código llama a esto - y el permiso del usuario la primera vez.

Una pantalla de lista de contactos. Muéstrala solo donde pueda funcionar. Cada contacto es { 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
  }
}

Busca en toda la agenda leyéndola página a página en una sola 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));
}

Combínalo con sendSms para enviar un mensaje al primer número de un contacto.

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!`);
}

Notas: Un resultado vacío no distingue "sin contactos" de "permiso rechazado": muestra una pista breve ("Permite los contactos en Ajustes") cuando la lista esté vacía. Los contactos vienen ordenados de la A a la Z por nombre; limit se limita a 2000. Para una sola persona usa pickContact(), que no necesita permiso. Contactos son datos personales: decláralos en tu formulario de seguridad de datos de Play y en tu política de privacidad.

appmintNative.listSms appmintNative#

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

Ejemplo

Lee mensajes SMS de la bandeja de entrada, enviados o borradores, de mĂĄs reciente a mĂĄs antiguo. Solo en la app de Android.

Devuelve: Promise<SmsMessage[]> - cada { id, address, body, date, read, box }. Vacío cuando SMS está desactivado, se rechaza el permiso o el buzón está vacío. Necesita: el interruptor SMS (leer y enviar) en el Paso 4 (Acceso) - la compilación lo activa cuando tu código llama a esto - y el permiso del usuario la primera vez.

Muestra los Ăşltimos 20 mensajes de la bandeja.

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
  }
}

Mensajes sin leer de un remitente, los más nuevos primero.

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);
}

Lee los enviados y los borradores de una vez, y ordĂŠnalos juntos por fecha.

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));
}

Notas: Una lista vacĂa no distingue "no hay mensajes" de "permiso denegado". La app limita limit a 500. can('smsList') es 'none' en la web. Los mensajes nuevos entrantes no forman parte de este wrapper; las pĂĄginas escritas a mano los reciben del evento appmint:sms-received (consulta listSms para el puente sin procesar). El SMS es un permiso restringido de Google Play: necesitas una DeclaraciĂłn de permisos aprobada en Play Console, que normalmente solo se concede a apps de SMS predeterminadas.

appmintNative.liveProgress appmintNative#

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

Ejemplo

Una tarjeta de estado fija y silenciosa en el panel de notificaciones de Android que actualizas en el sitio: una entrega, una subida, un temporizador, un entrenamiento. start, update, end.

Devuelve: start / update → Promise<boolean> (false cuando no está disponible, cuando las notificaciones no están permitidas o cuando el usuario desactivó su categoría "Estado en vivo"); end → Promise<void>. Requiere: la app de Android y el permiso de notificaciones en Android 13+. En la web can('liveProgress') es 'none': muestra allí el mismo estado dentro de tu app.

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

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

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

Un temporizador en marcha: startedAt muestra un reloj que cuenta hacia arriba desde ese momento.

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

Gestiona un rechazo. start responde false; liveProgress.lastError dice por qué.

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;
}

Notas: el mismo id actualiza una tarjeta; ids distintos crean tarjetas separadas (el id por defecto es 'live'). El usuario no puede descartarla deslizando, asà que llama siempre a end. Pide primero el permiso de notificaciones en Android 13+ (Notification.requestPermission()): sin él start responde false y no se muestra nada.

appmintNative.LocalNotifications appmintNative#

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

Ejemplo

La forma @capacitor/local-notifications (LocalNotifications.schedule({ notifications })) para el código escrito contra el plugin de Capacitor. Una notificación sin schedule se muestra ahora (a través de notify()); una con schedule: { at } es un recordatorio a esa hora, que se conserva tras reinicios.

Devuelve: Promise<{ notifications: { id }[] }>. Un recordatorio que no se puede establecer (un navegador, una compilaciĂłn sin Notificaciones, permiso denegado) se rechaza con el motivo: nunca se muestra antes de tiempo en su lugar. Recordatorios repetidos: scheduleAlarm o la API de recordatorios. Requiere: Android: Notificaciones activadas en el paso Acceso (Paso 4) al 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' },
    ],
  });
}

Varias notificaciones en una llamada, cada una reemplazando a una anterior que tenga el mismo 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' }] : []),
    ],
  });
}

Cuando necesites saber si se mostrĂł, usa notify() en su lugar y reacciona a la respuesta.

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
}

Notas: solo inmediatas: los campos de Capacitor id, schedule, at, actionTypeId y extra no son compatibles; cada elemento es { title, body?, icon?, tag? }. Para una notificaciĂłn en un momento posterior, una pĂĄgina escrita a mano usa WebToApk.scheduleNotificationEx (Android). Para botones usa notifyWithActions.

appmintNative.lockOrientation appmintNative#

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

Ejemplo

Fija la pantalla en una orientación para una vista que solo tiene sentido así: un tablero de juego, un gráfico ancho, un panel de firma. 'any' libera el bloqueo.

Devuelve: Promise<boolean> - true cuando se aplicĂł el bloqueo (o la liberaciĂłn), false cuando la plataforma lo rechazĂł. Nunca se rechaza. Requiere: nada.

Bloquea mientras la vista está abierta y libera al salir:

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>;
}

Comprueba la respuesta cuando la vista dependa de ella:

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

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

Libera el bloqueo cuando la pantalla desaparece, como un hook compartido por varias pantallas.

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

Notas:

  • En la app el plugin ScreenOrientation de Capacitor aplica el bloqueo.
  • En la web usa screen.orientation.lock(). La mayorĂa de los navegadores lo rechazan fuera de pantalla completa y los de escritorio siempre lo rechazan, asĂ que espera false allĂ.
  • Da siempre al usuario una forma de salir de una vista bloqueada.

appmintNative.longPress appmintNative#

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

Ejemplo

Te avisa cuando el usuario mantiene un dedo sobre un elemento: para el menú contextual de una fila, reacciones o acciones de una tarjeta.

Devuelve: una función para cancelar la suscripción: llámala al desmontar. Requiere: nada.

Opciones: ms - tiempo de pulsación, por defecto 450; haptics - respuesta táctil media cuando se activa, por defecto true.

Abre una hoja de acciones con una pulsación larga, con un botón visible para el mismo menú:

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

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

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

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

Una pulsación más lenta, sin vibración:

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 });

EmpĂĄrejalo con haptic para una confirmaciĂłn personalizada y limpia en cada fila de la 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>;
}

Notas:

  • Se dispara una vez por pulsaciĂłn y se traga el clic que vendrĂa despuĂŠs, asĂ que una pulsaciĂłn larga no cuenta tambiĂŠn como "toque".
  • Moverse más de 8 px lo cancela, así que al desplazarse nunca se activa. También bloquea el menú de pulsación larga del propio navegador en ese elemento.
  • Una pulsación larga es invisible. Deja siempre un botón visible para la misma acción.

appmintNative.markPushRead appmintNative#

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

Ejemplo

Marca como leído un mensaje de la bandeja de entrada de AppMint Push, o todos los mensajes cuando se llama sin argumento.

Devuelve: boolean de forma síncrona - true si algo cambió. Siempre false en la web. Necesita: Android: AppMint Push activado en el paso Integrar (Paso 3) al 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();
}

Cuenta los mensajes sin leer después de marcar uno, para una insignia en tu propio encabezado.

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 };
}

Marca todo como leído cuando el usuario abra la pantalla de la bandeja y actualiza cuando llegue un mensaje nuevo.

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;
}

Notas: el estado de lectura vive solo en este teléfono; la notificación de la bandeja no se elimina.

appmintNative.mediaSession appmintNative#

mediaSession — @/lib/appmintNative

Ejemplo

Controles de reproducciĂłn en la pantalla de bloqueo y en la notificaciĂłn para una app que reproduce audio o vĂdeo: publica lo que suena, recibe los botones Reproducir / Pausa / Siguiente / Anterior / Detener y pasa un vĂdeo a una TV.

Devuelve: setMetadata, setPlaybackState, clear → void. onAction(handler) → una función para cancelar la suscripción. cast(url) → Promise<boolean> (false cuando no se pudo ofrecer nada). Necesita: Notificación multimedia en el Paso 4 (Acceso) al compilar; las compilaciones con IA lo activan por ti cuando el 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} />;
}

Envía el vídeo actual a un televisor:

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>;
}

Borra la tarjeta de la pantalla de bloqueo cuando termina la reproducción y déjala como está cuando la plataforma no tiene controles multimedia.

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);
}

Notas:

  • En Android los botones llegan a través del navigator.mediaSession de la app, que se instala cuando la página termina de cargar. Un onAction registrado antes (por ejemplo en un efecto de montaje) se pierde, así que regístralo en la primera reproducción.
  • La acciĂłn seek (con positionMs) se entrega solo en la web; la notificaciĂłn de Android no tiene bĂşsqueda.
  • artworkUrl debe ser una direcciĂłn https:// completa en internet. En Android, cast(url) abre el selector "Transmitir a..." (un traspaso; se resuelve true una vez que se pide el selector). En la web usa la API Remote Playback.

appmintNative.Motion appmintNative#

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

Ejemplo

La forma @capacitor/motion sobre los eventos estándar devicemotion y deviceorientation: movimiento e inclinación del teléfono. El mismo código funciona en la app y en un navegador.

Devuelve: Promise<{ remove(): Promise<void> }> - llama a remove() para detener. 'accel' devuelve con { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }; 'orientation' con { alpha, beta, gamma } (grados). Requiere: nada.

Agita para actualizar.

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

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

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

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

Inclina una burbuja de nivel con el evento de orientación.

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 con limpieza automática para un componente de React que lee el movimiento del teléfono.

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 };
}

Notas: Algunos navegadores piden primero acceso al movimiento, asĂ que inĂcialo desde un toque allĂ; si el usuario lo rechaza, se devuelve el handle pero no llega ningĂşn evento. can('motion') es 'none' donde el navegador no tiene DeviceMotionEvent. Los valores de sensor que faltan llegan como 0. Llama siempre a remove() cuando se cierre la pantalla: los eventos de movimiento se disparan muchas veces por segundo.

appmintNative.musicVolume appmintNative#

musicVolume() — @/lib/appmintNative

Ejemplo

Lee el nivel de la música de fondo en bucle de la app, de 0 a 100.

Devuelve: number, de forma síncrona: 0-100, o -1 cuando no hay música (en la web, o en una app compilada sin archivo de música). Requiere: un archivo elegido en Música de fondo en el paso 3 (Integrar) al compilar.

Dibuja un control deslizante de música solo cuando hay 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>
  );
}

Mantén el nivel sincronizado con un interruptor de "Silenciar música", recordando el volumen al que volver.

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>;
}

Baja el volumen de la mĂşsica mientras se reproduce un vĂdeo y luego restĂĄuralo.

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));
}

Notas: solo en la app de Android; la web responde -1. Un nivel establecido con setMusicVolume se conserva para el siguiente inicio; hasta que la página establezca uno, es el nivel de la compilación.

appmintNative.navEnd appmintNative#

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

Ejemplo

Documentado junto con appmintNative.navStart: el ejemplo de allí también muestra este.

navStart() muestra la fina barra de carga superior de la app para un cambio de pantalla que la app no puede ver por sí sola; navEnd() la termina.

Devuelve: nada (ambos son sĂncronos). Requiere: nada. En la web (sin barra del shell de la app) ambos no hacen nada.

Úsalos solo para una espera en la que nada del DOM cambia hasta que llegan los datos, y combínalos siempre con 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
  }
}

Envuelve un envío que tarda un rato, para que el usuario vea la barra mientras trabaja el servidor.

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();
  }
}

Crea una pequeña función auxiliar que envuelva cualquier promesa.

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));

Notas: Llaman a WebToApk.navStarted() / navSettled(). La app ya controla la barra cuando tu app cambia de pantalla en el DOM, asĂ que la mayorĂa de los cambios de ruta no necesitan nada. No se dibuja nada para un cambio de menos de unos 200 ms, y una barra que nunca termina se detiene sola a los 2.5 segundos. Sigue dibujando tu propio indicador de carga o esqueleto; la barra es un extra.

appmintNative.navStart appmintNative#

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

Ejemplo

navStart() muestra la fina barra de carga superior de la app para un cambio de pantalla que la app no puede ver por sí sola; navEnd() la termina.

Devuelve: nada (ambos son sĂncronos). Requiere: nada. En la web (sin barra del shell de la app) ambos no hacen nada.

Úsalos solo para una espera en la que nada del DOM cambia hasta que llegan los datos, y combínalos siempre con 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
  }
}

Envuelve un envío que tarda un rato, para que el usuario vea la barra mientras trabaja el servidor.

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();
  }
}

Crea una pequeña función auxiliar que envuelva cualquier promesa.

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));

Notas: Llaman a WebToApk.navStarted() / navSettled(). La app ya controla la barra cuando tu app cambia de pantalla en el DOM, asĂ que la mayorĂa de los cambios de ruta no necesitan nada. No se dibuja nada para un cambio de menos de unos 200 ms, y una barra que nunca termina se detiene sola a los 2.5 segundos. Sigue dibujando tu propio indicador de carga o esqueleto; la barra es un extra.

appmintNative.Network appmintNative#

Network.getStatus(): Promise<{ connected, connectionType }> / Network.addListener(event, handler): { remove() }

Ejemplo

Estado en línea/sin conexión con la forma del plugin Network de Capacitor: el mismo código que networkStatus() / onNetworkChange().

Devuelve: getStatus() → Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) → la forma de Capacitor: una Promise del manejador que además tiene remove() por sí mismo; el manejador recibe la misma forma en cada cambio. Requiere: 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;
}

Lee el tipo actual una vez y muéstralo en una etiqueta de estado.

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>;
}

Retén una descarga grande con datos móviles y gestiona también el caso sin conexión.

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();
}

Notas: El nombre del evento se ignora: cada oyente recibe los cambios de conectividad. connectionType es 'wifi', 'cellular', 'none' o 'unknown': en la app getStatus() y el oyente usan el plugin Network, en la web navigator.onLine / navigator.connection.type. connected significa que el teléfono tiene red, no que tu servidor responda.

appmintNative.networkStatus appmintNative#

networkStatus(): { connected: boolean; type: string }

Ejemplo

Dice, ahora mismo, si el dispositivo tiene conexión.

Devuelve: { connected, type }, de forma síncrona. type es 'wifi', 'cellular', 'none' (sin conexión) o 'unknown'. Necesita: nada.

Lo bastante ligero para llamarlo durante el renderizado:

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>
  );
}

Lee el valor inicial y luego sigue los cambios:

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;
}

Elige un tamaño de imagen más ligero con datos móviles, respondiendo también el caso 'none' (sin conexión).

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'}`;
}

Notas:

  • En la app es la respuesta del plugin Network (se mantiene actualizada en segundo plano, así que la primerísima llamada aún puede leer el valor propio del WebView). En la web es navigator.onLine más navigator.connection.type cuando el navegador lo informa; si no, 'unknown': una clase de velocidad como '4g' nunca se informa como el tipo.
  • "Conectado" significa que el dispositivo tiene red, no que tu servidor responda. Gestiona igualmente las solicitudes fallidas.

appmintNative.nfc appmintNative#

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

Ejemplo

Funciones auxiliares de una sola llamada sobre Web NFC (NDEFReader): comprobar la compatibilidad, leer etiquetas, escribir una etiqueta de texto. La app de Android usa su propio NDEFReader; en la web es el Web NFC propio de Chrome (Chrome en Android).

Devuelve: isSupported(): Promise<boolean>; scan(onTag): Promise<() => Promise<void>> (la función de detención), y onTag recibe Array<{recordType, text, mediaType?}> por etiqueta: los nombres de tipo de Web NFC ('text', 'url', 'mime', …) y el texto decodificado en todas las plataformas; write(text): Promise<boolean> (true una vez escrita una etiqueta, false en cualquier fallo). Requiere: en Android, activa Compatibilidad con Web NFC en el paso 4 (Acceso) al compilar. scan() lanza un error cuando no hay NDEFReader: comprueba antes can('nfc').

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

export function TagReader() {
  const [last, setLast] = useState('');
  const stopRef = useRef<null | (() => Promise<void>)>(null);

  useEffect(() => () => { void stopRef.current?.(); }, []);

  // Check at the tap: on Android the app installs NDEFReader while the page loads.
  const start = async () => {
    if (can('nfc') === 'none' || !(await nfc.isSupported())) { setLast('NFC is not available here.'); return; }
    try {
      stopRef.current = await nfc.scan((records) => {
        setLast(records.filter((r) => r.recordType === 'text' || r.recordType === 'url').map((r) => r.text).join(' | '));
      });
      setLast('Hold a tag to the phone.');
    } catch (e) {
      setLast(`NFC error: ${(e as Error).message}`);
    }
  };

  const writeTag = async () => {
    if (can('nfc') === 'none') { setLast('NFC is not available here.'); return; }
    setLast('Tap a tag to write it...');
    setLast((await nfc.write('Hello from my app')) ? 'Tag written.' : 'Write failed or cancelled.');
  };

  return (
    <div>
      <button onClick={start}>Read tags</button>
      <button onClick={writeTag}>Write tag</button>
      <p>{last}</p>
    </div>
  );
}

Escribe un enlace en una etiqueta y dile al usuario por qué falló cuando falle.

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.');
}

Lee una etiqueta y luego deja 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);
  });
}

Notas: En Android isSupported() es true siempre que la compilación tenga NFC; no dice si NFC está activado (window.AndroidNFC.startScan() sí). Tras la función de parada, onTag no se vuelve a llamar nunca. En Android la propia app sigue captando etiquetas mientras está en primer plano (para que un toque no abra otra app), solo que deja de pasártelas. El text de un registro de texto no tiene prefijo de idioma; el text de un registro URL es la URL completa. write() espera hasta que se toque una etiqueta. Para escribir registros URL o mime, y para bloquear una etiqueta, usa NDEFReader directamente.

appmintNative.notify appmintNative#

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

Ejemplo

Muestra una notificación ahora mismo desde una app creada con IA, en la app de Android y en la web.

Devuelve: Promise<boolean> - true cuando se publicĂł, false cuando el usuario denegĂł el permiso de notificaciones, las notificaciones estĂĄn desactivadas para la app o no estĂĄn disponibles aquĂ. Pide el permiso por sĂ mismo la primera vez (Android 13+ y la web). Requiere: Android: Notificaciones activadas en el paso Acceso (Paso 4) al 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.');
}

Pregunta en un buen momento: el primer notify() muestra el diálogo de permiso. Para preguntar desde tu propio botón "Activar alertas", antes de publicar nada, usa la API estándar, que la app también admite:

async function enableAlerts() {
  if (typeof Notification === 'undefined') return false;
  const perm = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
  return perm === 'granted';
}

Abre la pantalla correcta cuando el usuario toca la notificación, combinando notify con onNotificationClick.

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

export async function announceMessage(id: string, from: string) {
  await notify({ title: `New message from ${from}`, tag: `msg-${id}`, data: { messageId: id } });
}

export function NotificationRouter() {
  const navigate = useNavigate();
  useEffect(() => onNotificationClick((e) => {
    const { messageId } = (e.data ?? {}) as { messageId?: string };
    if (messageId) navigate(`/messages/${messageId}`);
  }), [navigate]);
  return null;
}

Notas: Android usa el puente de notificaciones del shell (icon se convierte en el icono grande; usa una imagen https:// o data:); la web usa la Notification API. Cuando el usuario ha rechazado de forma definitiva, notify() se resuelve con false sin volver a preguntar: indícale los ajustes del teléfono. Para botones usa notifyWithActions; para una tarjeta de progreso fija usa liveProgress.

appmintNative.notifyWithActions appmintNative#

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

Ejemplo

Muestra una notificación con hasta 3 botones ("Marcar como hecho", "Posponer"). El toque vuelve a través de onNotificationAction.

Devuelve: Promise<boolean> - true cuando se publicĂł, false cuando se denegĂł el permiso de notificaciones (lo pide la primera vez, como notify()). Requiere: Android: Notificaciones activadas en el paso Acceso (Paso 4) al compilar, y el permiso del usuario en Android 13+. En la web can('notificationActions') es 'none'.

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

export async function remindTask(taskId: string, title: string) {
  if (can('notificationActions') === 'none') return false;   // web: hide the buttons
  return notifyWithActions({
    title,
    body: 'Due now',
    tag: `task-${taskId}`,                       // comes back as e.tag
    actions: [
      { id: 'done', title: 'Mark done' },
      { id: 'snooze', title: 'Snooze' },
    ],
  });
}

// Mount once (e.g. in App.tsx):
export const stopListening = onNotificationAction((e) => {
  // e = { actionId: 'snooze', tag: 'task-42', text: '' }
  const taskId = e.tag.replace('task-', '');
  if (e.actionId === 'done') completeTask(taskId);
  if (e.actionId === 'snooze') snoozeTask(taskId);
});

Un temporizador terminĂł: un botĂłn añade cinco minutos, el otro lo detiene.

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);
});

Gestiona un permiso rechazado después de la llamada y limpia en un componente.

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

export function ApprovalRequest({ requestId }: { requestId: string }) {
  const [note, setNote] = useState('');
  useEffect(() => onNotificationAction((e) => {
    if (e.tag === `req-${requestId}`) setNote(e.actionId === 'approve' ? 'Approved' : 'Rejected');
  }), [requestId]);

  const ask = async () => {
    const posted = await notifyWithActions({
      title: 'Approve request?', tag: `req-${requestId}`,
      actions: [{ id: 'approve', title: 'Approve' }, { id: 'reject', title: 'Reject' }],
    });
    if (!posted) setNote('Notifications are off, so no buttons were shown.');
  };
  return <button onClick={ask}>{note || 'Ask me'}</button>;
}

Notas: Cada botĂłn muestra su title; su id es lo que vuelve como e.actionId. La opciĂłn input (cuadro de respuesta en lĂnea) no es compatible con esta app: se ignora, el botĂłn no tiene cuadro de respuesta y e.text es siempre ''. Un toque llega a la pĂĄgina solo mientras la app se ejecuta. En la web el wrapper publicarĂa una notificaciĂłn simple sin botones, por eso el ejemplo comprueba antes can().

appmintNative.offlineBanner appmintNative#

offlineBanner(opts?: { message?: string; onlineMessage?: string }): () => void

Ejemplo

Monta una franja lista que se desliza bajo la barra de estado cuando se cae la conexiĂłn y dice brevemente "De nuevo en lĂnea" cuando vuelve.

Devuelve: una funciĂłn para cancelar la suscripciĂłn que elimina el banner y deja de escuchar. Requiere: nada.

Móntalo una sola vez al iniciar la app:

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

export function App() {
  useEffect(() => offlineBanner(), []);          // cleanup removes it
  return <main>{/* … */}</main>;
}

Tus propias palabras (por ejemplo en el idioma de la app):

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

const off = offlineBanner({ message: 'Sin conexión', onlineMessage: 'Conectado de nuevo' });

Monta el banner solo para la zona de la app con sesión iniciada y quítalo al cerrar sesión.

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]);
}

Notas:

  • Por defecto: "Sin conexión" y "De nuevo en línea". El mensaje de en línea se oculta tras unos 1,8 segundos; el de sin conexión permanece hasta que vuelva la conexión.
  • Si el dispositivo ya estĂĄ sin conexiĂłn cuando se monta, el banner se muestra de inmediato.
  • Construido sobre onNetworkChange(), así que es real en todas las superficies. Los colores vienen de los tokens de la app (--ink / --bg sin conexión, --accent / --accent-ink en línea). Tiene role="status" para lectores de pantalla.
  • Móntalo una sola vez. Dos llamadas dibujan dos banners.

appmintNative.onAlarm appmintNative#

onAlarm(cb) — @/lib/appmintNative

Ejemplo

Escucha los resultados de las alarmas: la app abierta por una alarma (action: 'open', con ringing), y cómo terminó un timbre: dismiss, snooze, missed (sonó hasta el final, o fue reemplazada por otra alarma) o error. Los resultados de cuando la app estaba cerrada se entregan la próxima vez que se carga la página.

Devuelve: una función que deja de escuchar. La función recibe { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. Necesita: 🔔 Despertador en el paso Acceso (Paso 4) al 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;
}

Establece una alarma, respondiendo antes a alarmSupport().

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

export function setWakeUp(at: Date): string {
  const support = alarmSupport();
  if (support === 'unavailable') return 'Alarms are only available in the Android app.';
  if (support !== 'ready') return `Alarm needs attention first (${support}).`;
  const ok = scheduleAlarm('wake-up', { at: at.getTime(), title: 'Wake up', repeat: 'weekdays', snoozeMinutes: 10 });
  return ok ? 'Alarm set.' : 'Could not set the alarm.';
}

Detén o pospón la alarma que suena desde tu propia pantalla, y reacciona al 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>
  );
}

Notas: los recordatorios normales que abren la app tambiĂŠn llegan aquĂ con action: 'open' y ringing: false.

appmintNative.onBackGesture appmintNative#

onBackGesture(handler: () => boolean): () => void

Ejemplo

Permite que una hoja, modal, lightbox o paso de asistente abierto responda primero al gesto de RETROCESO: el deslizamiento desde el borde o el botĂłn atrĂĄs en la app de Android, el botĂłn atrĂĄs del navegador en la web.

Devuelve: una funciĂłn para cancelar la suscripciĂłn: llĂĄmala cuando la vista se cierre o se desmonte. Tu controlador devuelve true para consumir la pulsaciĂłn (cerraste algo) o false para dejarla pasar. Requiere: nada.

Regístralo solo mientras la hoja esté abierta:

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>}
    </>
  );
}

Retrocede paso a paso por un asistente y deja que el retroceso del último paso salga con normalidad:

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]);
}

Cierra un visor de imágenes con el gesto de retroceso, manteniendo el registro ligado a que la imagen esté abierta.

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;
}

Notas:

  • Los gestores forman una pila: se pregunta primero al último registrado (la hoja de encima). Un gestor que lanza una excepción no consume nada.
  • En Android pasa por el controlador de retroceso del shell. Sin Ă©l, deslizar hacia atrás con una hoja abierta cierra toda la app.
  • En la web añade una entrada al historial mientras haya algún manejador registrado y la elimina cuando el último cancela su suscripción.
  • Dibuja siempre un control visible de volver / cerrar: muchos usuarios nunca usan el gesto de volver.
  • React Router ya gestiona el retroceso normal de página; usa esto solo para estados que el router no conoce. Cancela siempre la suscripción, o una hoja cerrada seguirá consumiendo los toques de retroceso.

appmintNative.onInAppMessage appmintNative#

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

Ejemplo

Te avisa por cada mensaje dentro de la app (las campañas del creador) que se muestra, se toca, se descarta o se responde en esta instalación. Devuelve la función para cancelar la suscripción.

Devuelve: () => void. El callback recibe {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. Nunca se dispara en la web. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al 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]);
}

Dibuja tĂş los mensajes (con setInAppPresentation('none'), presented es false y message lleva el diseĂąo):

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) };
}

Reacciona a un botón que el usuario tocó en el propio dibujo de la app:

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

onInAppMessage((e) => {
  if (e.type === 'clicked' && e.link === 'appmint://notifications/enable') refreshNotificationBadge();
});

Notas: button es -1 para un toque en el propio mensaje; si no, el índice del botón.

Respuestas (type: 'answered'). Un mensaje con una fila de estrellas (encuesta de estrellas, Valóranos) o fichas de puntuación 0-10 (puntuación NPS) informa de la respuesta una vez, al cerrarse: question es 'stars' (valor 1-5) o 'nps' (valor 0-10), y template nombra la plantilla de la galería de la que procede. El centro de envíos muestra la media o la puntuación NPS; tu código puede hacer seguimiento:

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

onInAppMessage((e) => {
  if (e.type === 'answered' && e.question === 'nps' && (e.value ?? 10) <= 6) openFeedbackSheet();
});

Lleva tu propio registro de las valoraciones con estrellas:

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 }) });
});

Agradece a quienes puntuaron alto, en un componente de 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

Ejemplo

Te dice cuántos píxeles de la página cubre el teclado en pantalla, cada vez que cambia, para que una barra inferior pueda subir por encima.

Devuelve: una función para cancelar la suscripción: llámala al desmontar. La función recibe un número en píxeles CSS: el relleno inferior extra que necesitas, 0 cuando el teclado está cerrado. Necesita: 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>
  );
}

MantĂŠn una lista de chat desplazada al mensaje mĂĄs reciente mientras se abre el teclado:

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

export function MessageList({ children }: { children: React.ReactNode }) {
  const endRef = useRef<HTMLDivElement>(null);
  useEffect(() => onKeyboard((inset) => {
    if (inset > 0) endRef.current?.scrollIntoView({ block: 'end' });   // keyboard just opened
  }), []);
  return <div className="overflow-y-auto">{children}<div ref={endRef} /></div>;
}

Un hook reutilizable, con un margen de ĂĄrea segura que desaparece mientras el teclado estĂĄ abierto:

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>

Notas:

  • En la app escucha el plugin Keyboard de Capacitor e informa solo de la parte del teclado que el diseño no absorbiĂł ya. El shell suele redimensionar la pĂĄgina para el teclado, asĂ que el valor allĂ suele ser 0; es correcto, no sumes tĂş la altura bruta del teclado.
  • En una app a pantalla completa o en modo quiosco (página sin redimensionar) informa de la altura total del teclado.
  • En la web usa visualViewport, llama al callback una vez de inmediato y trata menos de 80 px como interfaz del navegador (informado como 0).
  • En la app la primera llamada llega cuando el teclado se abre por primera vez, asĂ que empieza tu estado en 0.

appmintNative.onNetworkChange appmintNative#

onNetworkChange(cb: (status: { connected: boolean; type: string }) => void): () => void

Ejemplo

Te llama cada vez que la conexiĂłn se cae o vuelve: para un banner sin conexiĂłn o para reintentar escrituras en cola.

Devuelve: una función para cancelar la suscripción: llámala al desmontar. Requiere: nada.

Reintenta el trabajo en cola cuando el dispositivo vuelve a tener conexión:

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]);
}

Muestra el estado actual de inmediato y luego sigue los cambios (el callback no se llama al suscribirse):

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;
}

Retén las descargas grandes con datos móviles y trata 'none' como sin conexión:

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]);
}

Notas:

  • NO te llama con el estado actual al suscribirte. Lee networkStatus() para obtener el valor inicial.
  • En la app escucha el plugin Network de Capacitor (networkStatusChange); type es 'wifi', 'cellular', 'none' o 'unknown'.
  • En la web escucha los eventos online / offline e informa networkStatus().
  • Para un banner ya hecho, usa offlineBanner(), que está construido sobre esto.

appmintNative.onNotificationAction appmintNative#

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

Ejemplo

Escucha los toques en los botones de las notificaciones (desde notifyWithActions).

Devuelve: una función que deja de escuchar. La función recibe { actionId, tag }: el id del botón y la etiqueta de la notificación. La app de Android no envía text de respuesta (la respuesta en línea no es compatible), así que no dependas de e.text. Necesita: lo mismo 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 sin procesar en una página escrita a mano (app de Android):

window.addEventListener('appmint:notification-action', function (e) {
  // e.detail = { actionId: 'Mute', tag: 'chat-7' }
  console.log(e.detail.actionId, e.detail.tag);
});

Publica una notificación con botones solo donde la compilación los admita y gestiona el toque por la etiqueta de la notificación:

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]);
}

Notas: en la app de Android es el evento appmint:notification-action (también window.onAppMintNotificationAction(detail)); en la web no hace nada. Un toque se entrega solo mientras la app está en ejecución: tocar un botón no abre la app.

appmintNative.onNotificationClick appmintNative#

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

Ejemplo

Ejecuta una función cuando el usuario toca una notificación publicada por la app, incluido el toque que inició la app en frío, para que la app pueda abrir la pantalla correcta.

Devuelve: la funciĂłn para cancelar la suscripciĂłn. El callback recibe { tag, title, body, data }; data es lo que pasaste a notify({ …, data }). Requiere: notificaciones, como notify(). Evento que hay detrĂĄ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 } });
}

Abre la pantalla correcta tras el toque, incluso cuando iniciĂł la app en frĂo, e ignora las notificaciones que no llevan datos:

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]);
}

EnvĂa una notificaciĂłn solo donde existan las notificaciones, con datos para el toque (el data necesita un tag):

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

export async function announceMessage(chatId: string, from: string, text: string) {
  if (can('notify') === 'none') return;                 // nothing to tap on this surface
  const shown = await notify({ title: from, body: text, tag: `chat-${chatId}`, data: { screen: 'chat', id: chatId } });
  if (!shown) toast('Notifications are turned off');
}

Notas: El toque viaja a través de la pantalla de presentación y de la pantalla del PIN hasta la página. data debe ser JSON (menos de 16 KB) y necesita un tag.

appmintNative.onPullToRefresh appmintNative#

onPullToRefresh(el: HTMLElement, onRefresh: () => void | Promise<void>, opts?: { threshold?: number }): () => void

Ejemplo

Añade el gesto de arrastrar hacia abajo para actualizar a una lista con desplazamiento: arrastra hacia abajo en la parte superior, suelta y tus datos se recargan.

Devuelve: una función para cancelar la suscripción: llámala al desmontar. Requiere: nada.

el es el elemento que se desplaza (o document.documentElement cuando se desplaza toda la pĂĄgina). El indicador de carga permanece hasta que la promesa de onRefresh se resuelve:

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

export function Inbox({ reload }: { reload: () => Promise<void> }) {
  const listRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!listRef.current) return;
    return onPullToRefresh(listRef.current, async () => {
      try { await reload(); }
      catch { toast('Could not refresh — check your connection'); }
    });
  }, [reload]);

  return <div ref={listRef} className="h-full overflow-y-auto">{/* rows */}</div>;
}

Una página que se desplaza como un todo, con un arrastre más largo:

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

declare function reloadFeed(): Promise<void>;
const off = onPullToRefresh(document.documentElement, reloadFeed, { threshold: 96 });

Actualiza una lista mediante un hook de datos, con un toque hĂĄptico cuando llegan los datos:

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">
}

Notas:

  • Solo empieza cuando la lista estĂĄ desplazada hasta arriba del todo. El threshold por defecto es 72 px de arrastre.
  • Dibuja un indicador redondo con los colores de la app (--surface, --accent), lanza una vibración media en el punto de confirmación e ignora nuevos gestos de arrastre mientras se ejecuta una actualización.
  • Actualiza tus DATOS, no la página. El gesto de arrastre para recargar la página del propio shell (Activar arrastrar para actualizar en el paso 3 (Integrar)) está desactivado por defecto: déjalo desactivado cuando uses esto, para que un arrastre no recargue también la página.
  • Basado en Pointer Events, asĂ que es igual en todas las superficies. Establece overscroll-behavior-y: contain en el y lo restaura al cancelar la suscripciĂłn.

appmintNative.onShared appmintNative#

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

Ejemplo

Te avisa cuando otra app comparte texto o un enlace con esta app mientras ya estĂĄ abierta.

Devuelve: una funciĂłn para cancelar la suscripciĂłn: llĂĄmala al desmontar. Requiere: la opciĂłn Recibir contenido compartido de otras apps de la compilaciĂłn con "Texto y enlaces"; se activa automĂĄticamente cuando tu cĂłdigo llama a onShared( o sharedText(.

Gestiona ambos casos: un contenido compartido que inició la app y otro que llega más tarde:

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]);
}

Una página escrita a mano puede escuchar ella misma el evento del shell:

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

Rellena previamente un formulario de "nueva nota" desde un enlace compartido, solo donde la compilación pueda recibir contenido compartido:

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)} />;
}

Notas:

  • Android: escucha el evento appmint:shared del shell; el shell también llama a window.onAppMintShared(detail) cuando una página lo define. La misma carga también se conserva para sharedText() hasta clearShared().
  • En la web nunca se dispara y devuelve una funciĂłn de cancelaciĂłn que no hace nada.

appmintNative.onVolumeChange appmintNative#

onVolumeChange(cb) — @/lib/appmintNative

Ejemplo

Te llama con cb cada vez que cambia uno de los niveles de volumen del telĂ©fono: las teclas fĂsicas, el control del sistema u otra app.

Devuelve: una funciĂłn que deja de escuchar (la Ăşltima en detenerse apaga la vigilancia nativa). El callback recibe { stream, current, max, percent, muted? } por cada secuencia que cambiĂł. No hace nada en la web. Requiere: 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>;
}

Avisa cuando el volumen multimedia estĂĄ al mĂnimo mientras se reproduce un vĂdeo, y deja de escuchar al 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]);
}

Refleja el nivel del teléfono en un control deslizante y escribe de vuelta el cambio del usuario (la web no tiene volumen, así que getVolume es null allí):

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>

Ejemplo

Abre la pantalla de ajustes de notificaciones de esta app (app de Android).

Devuelve: Promise<boolean> - true cuando se abriĂł la pantalla; false en la web y donde no exista esa pantalla. Comprueba can('notificationSettings') antes de dibujar el control. Requiere: nada.

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

export function NotificationSettingsRow(row: HTMLElement) {
  row.hidden = can('notificationSettings') === 'none';
  row.addEventListener('click', () => { void openNotificationSettings(); });
}

Notas: para un "Activar notificaciones" de un solo toque usa enableNotifications(), que abre esta pantalla solo cuando ya no se puede mostrar el cuadro de permiso.

appmintNative.passkeys appmintNative#

passkeys — available / create / get / origin / assetLinks

Ejemplo

Inicio de sesiĂłn y registro con passkey usando las opciones JSON que genera tu servidor de passkeys. En la app de Android usa el gestor de passkeys de Android; en la web usa navigator.credentials.

Devuelve: available() → 'native' | 'web' | 'none'; create(optionsJSON) → Promesa del RegistrationResponseJSON; get(optionsJSON) → Promesa del AuthenticationResponseJSON (envía cualquiera de los dos por POST a tu servidor). Se rechazan con una DOMException (NotAllowedError, InvalidStateError, SecurityError, NotSupportedError). origin() → cadena, assetLinks() → arreglo (solo en la app; '' / [] en la web). Necesita: un servidor de passkeys; en la app, además, Android 9+ con Google Play services y el de tu dominio assetlinks.json.

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

export async function signInWithPasskey(): Promise<boolean> {
  if (passkeys.available() === 'none') return false;          // hide the passkey button
  const options = await (await fetch('https://your-domain.com/passkey/login-options')).json();
  try {
    const answer = await passkeys.get(options);
    const res = await fetch('https://your-domain.com/passkey/login-verify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answer),
    });
    return res.ok;
  } catch (e) {
    if ((e as DOMException).name === 'NotAllowedError') return false;   // cancelled / none here
    throw e;
  }
}

export async function addPasskey() {
  const options = await (await fetch('https://your-domain.com/passkey/register-options')).json();
  const answer = await passkeys.create(options);   // InvalidStateError = already registered
  await fetch('https://your-domain.com/passkey/register-verify', {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answer),
  });
}

Valores de configuración inicial, desde la app instalada mediante 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

Ofrece el botĂłn de passkey solo donde pueda funcionar, y dilo cuando no pueda:

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>;
}

Notas: En la web necesita una página segura (https://). Una página incluida en la app no tiene dominio: las opciones de tu servidor deben establecer rp.id / rpId con tu dominio. El autocompletado de passkeys (mediation: 'conditional') no está disponible en la app: empieza desde un botón.

appmintNative.paste appmintNative#

paste(): Promise<string>

Ejemplo

Lee el texto del portapapeles del sistema.

Devuelve: Promise<string> - el texto del portapapeles, o '' cuando está vacío. Se RECHAZA cuando este navegador no puede leer el portapapeles en absoluto, o cuando el usuario deniega el permiso. Requiere: 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>
  );
}

La forma al estilo Capacitor:

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

const { value } = await Clipboard.read();

Pega en un cuadro de texto con un mensaje para cada resultado y combínalo con 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');
}

Notas: En la app el plugin Clipboard lee el texto; un portapapeles vacĂo da '', no un error. En un navegador es navigator.clipboard.readText() (solo Chromium), que pide permiso al usuario: llĂĄmalo desde un toque.

appmintNative.pickContact appmintNative#

pickContact(): Promise<Contact | null>

Ejemplo

Abre el selector de contactos del sistema para que el usuario elija a una persona: el selector de Android en la app, la Contact Picker API en Chrome en Android.

Devuelve: Promise<Contact | null> - null cuando el usuario cancela o se rechaza el acceso. Necesita: nada (el selector del sistema no necesita permiso).

Elige a una persona y rellena un formulario. El contacto tiene la misma forma en la app y en la web: { id?, name?, phones?: string[], emails?: string[], photoUri? }.

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

export async function onPickClick(setName: (s: string) => void, setPhone: (s: string) => void) {
  if (can('contactPick') === 'none') return;   // hide the button instead
  try {
    const c = await pickContact();
    if (!c) return;                            // cancelled
    setName(c.name ?? '');
    setPhone(c.phones?.[0] ?? '');
  } catch {
    // only a 5-minute timeout rejects
  }
}

Un botón "Elegir un contacto" que muestra la elección y un cuadro de número simple donde no existe el selector:

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>
  );
}

Pasa el número elegido a la app de mensajes, con un aviso cuando el contacto no tiene ninguno:

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)}`;
}

Notas: En la app solo vuelve el nĂşmero tocado, nunca los correos, y photoUri es una direcciĂłn content:// de la foto del contacto. can('contactPick') es 'none' en navegadores sin la API Contact Picker (escritorio, Safari): no dibujes el botĂłn allĂ.

appmintNative.pickFile appmintNative#

pickFile(accept?) from '@/lib/appmintNative'

Ejemplo

Permite al usuario elegir un archivo y leerlo como texto. En la app abre el selector de documentos de Android; en la web abre un campo de archivo normal.

Devuelve: Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }, o null solo cuando el usuario cancela. Rechaza con un Error cuyo code es 'no-picker' (el teléfono no tiene una app selectora de documentos), 'disabled' o 'failed' cuando no pudo abrirse ningún selector. text() lee el archivo completo y rechaza con el código del puente (por ejemplo 'not-found') cuando no puede. Necesita: nada. Con Acceso nativo a carpetas (SAF) marcado en el Paso 4 (Acceso), la app usa el selector de documentos de Android; sin él usa el selector de archivos de la propia 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)}`);
  }
}

Importa una copia de seguridad JSON y gestiona cada forma en que pickFile puede fallar:

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');
  }
}

Elige un archivo de texto y pĂłnlo en un editor, junto a los detalles del archivo:

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>
  );
}

Notas: En la app text() lee el archivo en bloques de 4 MB hasta el final y lo decodifica como UTF-8, así que los archivos grandes se leen completos: mantén tu propio límite de tamaño para lo que la pantalla pueda manejar. Un selector que no puede abrirse se rechaza, así que envuelve también pickFile() en try/catch. En la web text() es el File.text() propio del navegador. Para archivos binarios o para escribir de vuelta, las páginas escritas a mano usan WebToApkFS.

appmintNative.pip appmintNative#

pip.enter(video?) / pip.exit(video?)

Ejemplo

Mantiene un vĂdeo reproduciĂŠndose en una pequeĂąa ventana flotante mientras el usuario hace otra cosa.

Devuelve: enter() devuelve Promise<boolean> - true cuando se abrió la ventana flotante. exit() devuelve Promise<void>. Ninguno rechaza. Necesita: en la app de Android, la opción Picture-in-Picture (PiP) de la compilación en el Paso 3 (Integrar); se activa automáticamente cuando tu código llama a pip.enter(.

Un botón "minimizar" en un reproductor:

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>
  );
}

Sal de él cuando se cierre el reproductor (web):

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

declare const video: HTMLVideoElement;
await pip.exit(video);

Entra automĂĄticamente en imagen en imagen cuando el usuario se va durante la reproducciĂłn de un vĂdeo, solo donde sea compatible:

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]);
}

Notas:

  • Android (8+): TODA la app se encoge en la ventana flotante, y el argumento video se ignora. Devuelve false en versiones anteriores de Android o en una compilación sin la opción PiP. El usuario expande la ventana para volver; exit() no hace nada ahí.
  • La web: el picture-in-picture propio del elemento <video>. El vídeo debe estar cargado (normalmente reproduciéndose); una llamada sin video devuelve false.
  • Llámalo desde un toque.

appmintNative.platform appmintNative#

platform(): 'android' | 'ios' | 'web'

Ejemplo

Te dice en quĂŠ superficie se ejecuta el cĂłdigo: la app de Android instalada o un navegador web.

Devuelve: una cadena, al instante: 'android' dentro de la app, 'web' en un navegador. Pregunta primero a Capacitor (Capacitor.getPlatform()), luego busca el puente del shell de Android; si no, 'web'. El tipo también incluye 'ios', pero una app de AppMint nunca lo recibe: AppMint solo compila apps de Android. Requiere: nada.

Elige el enlace correcto y cópialo.

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';

Muestra controles solo de la 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>;
}

Etiqueta los eventos de analĂtica con la superficie.

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 }),
  });
}

Notas: platform() nunca responde 'web' dentro de la app instalada. No lo uses para decidir si una función existe: muchas funciones dependen además de interruptores de la compilación; can('<capability>') responde eso.

appmintNative.privacyScreen appmintNative#

privacyScreen — enable() / disable() / isEnabled()

Ejemplo

Bloquea capturas de pantalla, grabaciĂłn de pantalla y la vista previa del selector de apps mientras estĂĄ abierta una pantalla con secretos.

Devuelve: enable() → Promise<boolean> (false donde nada puede bloquearlo); disable() → Promise<void>; isEnabled() → Promise<boolean>. Requiere: nada. En la app de Android usa el bloqueo de capturas del shell (capturas de pantalla, grabación de pantalla, transmisión y la miniatura de recientes).

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
  }, []);
}

Dile al usuario qué está protegido, solo cuando realmente lo esté:

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' : '';
}

Activa la protección para una vista sensible e informa al usuario si no se pudo aplicar:

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();
}

Notas: En la app el bloqueo pertenece a la página actual y se libera cuando la app navega a otra ruta, así que actívalo de nuevo cuando esa pantalla vuelva a mostrarse. En la web can('privacyScreen') es 'none' y enable() devuelve false: no afirmes que hay protección allí. Nada impide que una cámara apunte a la pantalla.

appmintNative.pushInbox appmintNative#

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

Ejemplo

Devuelve los mensajes de AppMint Push que recibiĂł esta instalaciĂłn (los mĂĄs nuevos primero, hasta 50), para una pantalla de notificaciones dentro de la app.

Devuelve: PushMessage[] de forma sĂncrona: { id, title, body, imageUrl, link, receivedAt, read }. Un array vacĂo en la web y cuando AppMint Push estĂĄ desactivado. Requiere: Android: AppMint Push activado en el paso Integrar (Paso 3) al 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 };
}

Muestra un contador de no leídos que sigue a las nuevas llegadas:

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;
}

Dibuja la lista, con un estado vacĂo para la web y para compilaciones sin AppMint Push, y un botĂłn "Marcar todo como leĂdo":

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>
  );
}

Notas: la bandeja de entrada es la fuente de verdad guardada: léela al montar; el evento appmint:push solo se dispara mientras la app está en pantalla. Los mensajes descartados se omiten.

appmintNative.readBarcodeFrom appmintNative#

readBarcodeFrom(source: Blob | CanvasImageSource, formats?: BarcodeFormat[]): Promise<{ value, format }[]>

Ejemplo

Lee códigos de barras y códigos QR de una imagen que la página ya tiene: un archivo de foto, un Blob, un <img>, un <canvas> o un fotograma de vídeo. No se abre ninguna cámara.

Devuelve: Promise<{ value: string; format: string }[]> - todos los cĂłdigos encontrados, en el orden del detector; un array vacĂo cuando no hay ninguno, cuando no se puede leer la imagen o cuando la lectura de cĂłdigos de barras no estĂĄ disponible. Nunca se rechaza. Requiere: nada.

Lee un código de una foto que elige el usuario:

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>
  );
}

Desde una imagen que ya está en la 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);

Lee un código de un fotograma de canvas, comprobando primero qué admite el dispositivo:

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

export async function scanCanvas(canvas: HTMLCanvasElement): Promise<string | null> {
  const supported = await barcodeFormats();
  if (supported.length === 0) return null;                     // no reader on this device
  const wanted = ['qr_code', 'ean_13'].filter((f) => supported.includes(f)) as ('qr_code' | 'ean_13')[];
  const [hit] = await readBarcodeFrom(canvas, wanted);
  return hit?.value ?? null;
}

Notas:

  • Usa el BarcodeDetector del motor del navegador (presente en el WebView de la app de Android; falta en Safari y en muchos navegadores de escritorio; entonces devuelve []).
  • Un <input type="file" accept="image/*"> normal abre el selector de fotos de Android y no necesita permiso de almacenamiento.
  • Acota formats cuando los conozcas: es más rápido y se equivoca menos. barcodeFormats() enumera lo que admite el dispositivo.

appmintNative.readOtp appmintNative#

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

Ejemplo

Espera el código de un solo uso por SMS para esta app y lo devuelve: la hoja de consentimiento de SMS de Google en la app de Android, WebOTP en Chrome en la web.

Devuelve: Promise<string | null> - el código, o null cuando el usuario lo rechazó, pasaron cinco minutos o signal lo abortó. Lanza un error cuando can('otp') es 'none'. Requiere: nada que activar. El SMS debe terminar con @<host> #<code> (en la app de Android el host es 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" />
  );
}

Gestiona la respuesta "sin cĂłdigo" y ofrece reenviar cuando la espera termina sin ninguno:

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>
  );
}

Captura la excepción cuando no puedas comprobar 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
  }
}

Notas: es un acelerador: deja siempre el campo editable a mano.

appmintNative.recognizeText appmintNative#

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

Ejemplo

Lee el texto de una imagen en el dispositivo (can('ocr')): el reconocedor ML Kit de la app de Android, o el TextDetector de Chrome donde esté activado.

Devuelve: Promesa de { text, blocks: [{ text, box, lines: [{ text, box }] }] }, cuadros en píxeles de imagen. Rechaza con code 'unavailable' donde can('ocr') es 'none', o con una DOMException cuando falla el reconocimiento. Necesita: nada, sin permisos.

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>
  );
}

Gestiona explícitamente los casos de "sin reconocedor" y de fallo:

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
  }
}

Lee una escritura que no sea latina y lista cada lĂnea con su posiciĂłn:

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 }));
}

Notas: Solo escrituras latinas. La primera llamada en un teléfono nuevo puede tardar unos segundos mientras Play services descarga el modelo: muestra el progreso.

appmintNative.remoteConfig appmintNative#

remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()

Ejemplo

Dibuja la franja de anuncios y la pantalla de mantenimiento que el creador publica desde Remote Update, para la pĂĄgina cuando se ejecuta en un navegador web. En la app de Android el shell ya lo hace por sĂ mismo, asĂ que estas llamadas no hacen nada allĂ.

Devuelve: start(manifestUrl?) → Promise<void> (obtiene + dibuja; nunca se rechaza); fetch(url) → Promise<RemoteConfig | null> (nunca se rechaza; null = no se aplica nada); apply(cfg) → una función de limpieza; manifestUrl() → string (el content de <meta name="appmint-remote-manifest">, '' cuando no hay ninguno); handledHere() → boolean; dismissed(id) → boolean; dismiss(id) → void. Requiere: nada en la app: la actualización remota es nativa allí.

Se puede llamar sin riesgo al iniciar; en la app vuelve al instante:

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>;
}

O lee los valores y dibuja tu propia interfaz (solo web):

import { remoteConfig, type RemoteConfig } from '@/lib/appmintNative';

export async function loadNotice(url: string): Promise<RemoteConfig | null> {
  if (!remoteConfig.handledHere()) return null;         // the Android app draws its own
  const cfg = await remoteConfig.fetch(url);
  if (!cfg || !cfg.announcementText) return cfg;
  return remoteConfig.dismissed(cfg.announcementId) ? null : cfg;
}
// when the user closes your banner: remoteConfig.dismiss(cfg.announcementId)

Muestra tú mismo el texto de mantenimiento en la web y deja que la app use su propia pantalla:

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;
}

Notas:

  • RemoteConfig = { version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }.
  • handledHere() es false en la app de Android: el shell lee el manifiesto de forma nativa y dibuja el anuncio él mismo, así que nunca verás dos banners.
  • start() sin argumento usa manifestUrl(). Una compilación de AppMint no añade esa etiqueta meta, así que sin argumento (o tu propio <meta name="appmint-remote-manifest" content="…">) no se obtiene nada.
  • fetch() devuelve null para una publicación en pausa o revertida, un manifiesto cuya lista platforms no incluye web, un formato de manifiesto más nuevo o cualquier error de red.
  • El modo de mantenimiento gana: apply() cubre entonces toda la página con el texto de mantenimiento. Un anuncio descartado permanece oculto hasta que se publica un nuevo announcementId.

appmintNative.requestDndAccess appmintNative#

requestDndAccess() — @/lib/appmintNative

Ejemplo

Pide acceso a No molestar, que Android exige antes de que la app pueda cambiar el volumen del timbre o de las notificaciones mientras No molestar está activado.

Devuelve: 'granted', 'opened' (se mostró la pantalla de ajustes: vuelve a comprobar cuando el usuario regrese), 'not_declared', o 'unavailable' en la web. Requiere: nada que marcar: al llamarlo en tu código, la compilación declara el permiso.

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';
}

Pregunta una vez y luego reintenta el cambio de volumen cuando el usuario vuelva de los ajustes:

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;
  };
}

Bifurca segĂşn cada respuesta que pueda 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>

Ejemplo

Pide a Google Play que muestre su propia hoja de valoraciĂłn dentro de la app (Play In-App Review).

Devuelve: Promise<boolean> - true solo cuando Play mostró la hoja; false en la web, en una instalación que no vino de Google Play, cuando Play la rechazó (su cuota) o falló. Usa askForReview() para saber cuál fue el caso. Requiere: nada; la hoja solo aparece en una app instalada desde 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();
  }
}

Averigua por qué no se mostró la hoja, sin enviar al usuario a otra parte:

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);
}

Pregunta solo después de un momento de éxito, y como máximo una vez al mes:

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();
}

Notas: Google Play limita la frecuencia de la hoja y a menudo no muestra nada: nunca recompenses una reseña ni crees un botón "Valóranos" que espere que se abra la hoja (usa askForReview({ openStore: true }) para un botón). Nunca sale de la app hacia la ficha de Play.

appmintNative.restorePushMessage appmintNative#

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

Ejemplo

Deshace dismissPushMessage(id): el mensaje vuelve a la bandeja de AppMint Push en su lugar anterior.

Devuelve: boolean de forma sĂncrona: true si se restaurĂł; false si el id es desconocido o no se habĂa descartado (siempre false en la web). Requiere: Android: AppMint Push activado en el paso Integrar (Paso 3) al 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();
}

Ofrece Deshacer justo despuĂ©s de descartar deslizando, en una pequeña fila de 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>
  );
}

Comprueba la respuesta, porque un id desconocido o nunca descartado da 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)`;
}

Notas: los mensajes eliminados con clearPushInbox() o retirados por ti no se pueden restaurar.

appmintNative.safeArea appmintNative#

safeArea(): { top: number; right: number; bottom: number; left: number }

Ejemplo

Da el espacio ocupado por la barra de estado, el recorte de la cámara y la barra de navegación, en píxeles CSS, para que las barras fijas no queden debajo.

Devuelve: { top, right, bottom, left } números, de forma síncrona. Necesita: nada.

La mayorĂa de las veces basta con CSS: el runtime pone --safe-top, --safe-right, --safe-bottom, --safe-left en :root:

export function BottomNav() {
  return (
    <nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
      {/* tabs */}
    </nav>
  );
}

Usa los números cuando el código deba hacer las cuentas, por ejemplo para colocar un botón flotante:

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;
}

Mantén una lista con desplazamiento libre de las barras dimensionando su relleno a partir de los 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>;
}

Notas:

  • Se mide a partir de env(safe-area-inset-*) de CSS. El valor se guarda en caché y se vuelve a medir tras cambiar el tamaño de la ventana o girarla.
  • Antes de que exista document.body (muy pronto al importar) devuelve todo ceros: llámalo desde un efecto.
  • En un navegador de escritorio los cuatro suelen ser 0.

appmintNative.saveFile appmintNative#

saveFile(fileName, data, mime?) from '@/lib/appmintNative'

Ejemplo

Guarda texto o un Blob como un archivo que el usuario conserva, con el nombre que elijas. En la app abre la hoja "Guardar como…" de Android con ese nombre; en un navegador es una descarga normal.

Devuelve: (síncrono) boolean - true cuando se inició la descarga, false cuando no hay document o su creación lanzó un error. No te dice si el usuario guardó o canceló la hoja. Requiere: 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.');
}

Un Blob conserva su propio tipo (el tercer argumento se ignora):

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

const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
saveFile('Backup.json', blob);

Guarda el resultado de una petición fetch y avisa cuando no pudo iniciarse:

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');
}

Notas: Por debajo es un <a download> normal, que la app convierte en la hoja "Guardar como…" (consulta downloads). Pasa siempre un nombre con extensión; sin ella el archivo se guarda como download. El tipo MIME predeterminado para una cadena es text/plain.

appmintNative.saveToGallery appmintNative#

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

Ejemplo

Guarda una foto o un vídeo en la galería del teléfono: can('gallery') es 'native' en la app de Android y 'web' en un navegador (allí el archivo se descarga).

Devuelve: Promesa del URI del elemento guardado ('' en la web). Rechaza con code 'permission-denied', 'download-failed', 'bad-source' o 'failed'. Necesita: nada; sin permisos en 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>
  );
}

Guarda un clip de vídeo y gestiona los códigos de error con los que puede rechazarse:

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');
  }
}

Guarda una imagen desde una URL de datos y pásala a la hoja de compartir solo cuando se haya guardado:

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' });
}

Notas: source tambiĂ©n puede ser una URL data: o una URL http(s); pasa { kind: 'video' } para la URL de un vĂdeo sin tipo MIME de vĂdeo.

appmintNative.scanBarcode appmintNative#

scanBarcode(video: HTMLVideoElement, opts?: { signal?, formats?, facingMode? }): Promise<{ value, format } | null>

Ejemplo

Abre la cĂĄmara en un <video> que TĂ colocas en tu diseĂąo y se resuelve con el primer cĂłdigo de barras o QR que ve.

Devuelve: Promise<{ value: string; format: string } | null> - el primer cĂłdigo, o null cuando se cancela (el signal se abortĂł), cuando se denegĂł la cĂĄmara o cuando el escaneo no estĂĄ disponible. Nunca se rechaza y siempre detiene la cĂĄmara. Requiere: acceso a la cĂĄmara: en la app de Android activa CĂĄmara en el Paso 4 (Acceso) al compilar.

Úsalo solo cuando el visor deba estar dentro de tu propia pantalla; si no, scanCode() es más sencillo.

import { useEffect, useRef, useState } from 'react';
import { can, scanBarcode, haptic } from '@/lib/appmintNative';

export function InlineScanner({ onCode }: { onCode: (v: string) => void }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const video = videoRef.current;
    if (!video || can('barcodeScan') === 'none') return;
    const ctl = new AbortController();
    void scanBarcode(video, { signal: ctl.signal, formats: ['qr_code'] }).then((hit) => {
      if (hit) { haptic('success'); onCode(hit.value); }
      else if (!ctl.signal.aborted) setError('Camera not available. Type the code instead.');
    });
    return () => ctl.abort();          // leaving the screen stops the camera
  }, [onCode]);

  return (
    <div>
      <video ref={videoRef} className="aspect-square w-full object-cover" muted playsInline />
      {error && <p role="alert">{error}</p>}
    </div>
  );
}

Usa la cĂĄmara frontal:

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

declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });

Cierra el escáner tras el primer acierto y ofrece un código escrito a mano cuando la cámara no está disponible:

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>
  );
}

Notas:

  • Llama a getUserMedia (así el usuario ve el aviso de permiso de la cámara) y decodifica con BarcodeDetector en cada fotograma. can('barcodeScan') es 'none' donde falte cualquiera de los dos.
  • La compilación analiza tu código: una llamada a scanBarcode( activa Cámara por ti. Si la desactivaste a mano, la solicitud de cámara falla y recibes null.
  • Inícialo desde una acción del usuario (un botón Escanear o al abrir una pantalla de escaneo), nunca al iniciar la app. Trata null como un resultado normal, con una opción de introducción manual.

appmintNative.scanCode appmintNative#

scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>

Ejemplo

Abre un escáner a pantalla completa y resuelve con el primer código de barras o QR: una sola llamada, nada que dibujar.

Devuelve: Promise<ScanCodeResult | null> - { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }, o null cuando el usuario cerró el escáner. Cualquier otro caso se rechaza con un Error cuyo code es 'unavailable' (no hay escáner aquí: faltan los servicios de Play en la app; no hay BarcodeDetector ni cámara en la web), 'busy' (ya hay un escaneo abierto), 'camera-denied' (web: permiso de cámara rechazado) o 'failed'. Requiere: nada en la app (el escáner de Google no necesita permiso de cámara).

Desde un botón 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>
  );
}

Usa las partes ya analizadas que rellena la app (Wi-Fi, enlaces, contactos…), con una alternativa escrita a mano en el escáner de 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);

Pospón desde un botón en tu propia pantalla de alarma, usando onAlarm para saber cuándo está sonando.

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 cuando nada está sonando, así que comprueba antes de prometer al usuario un segundo timbre.

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.';
}

Notas:

  • En la app este es el escáner de códigos de Google (Play services, a través del AppMint.scanCode del shell): zoom automático, sin solicitud de cámara. type es 'url', 'wifi', 'text', 'phone', 'email', 'sms', 'geo', 'contact', 'calendar', 'isbn', 'product' o 'driver_license', y se rellena el campo correspondiente. manualInput permite al usuario escribir el código.
  • En la web dibuja su propia capa de cámara a pantalla completa (botón de cerrar, el gesto de retroceso la cierra) y decodifica con BarcodeDetector; allí solo se rellenan value y format, y el navegador pide permiso de cámara.
  • null siempre significa que el usuario cerró el escáner; cualquier otro resultado rechaza con un code, así que muestra un mensaje en catch.
  • LlĂĄmalo desde un toque, nunca al cargar la pĂĄgina. Acota formats cuando los conozcas.

appmintNative.scheduleAlarm appmintNative#

scheduleAlarm(id, input) — @/lib/appmintNative

Ejemplo

Establece una alarma real desde una app creada con IA: suena con el volumen de alarma del teléfono (incluso en modo silencio y, cuando el usuario permite las alarmas, a través de No molestar), sigue sonando hasta Descartar / Posponer o ringSeconds, y muestra la app en pantalla sobre la pantalla de bloqueo.

Devuelve: boolean, de forma sĂncrona: true cuando se estableciĂł; false en la web, en iOS o cuando alarmSupport() no es 'ready'. El mismo id reemplaza una alarma anterior. Requiere: 🔔 Despertador en el paso Acceso (Paso 4) al 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';
}

Ofrece la alarma solo cuando la app pueda hacerla sonar, y explica lo que falta en caso contrario:

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

const HELP: Record<string, string> = {
  not_enabled: 'Alarm clock is switched off in this build.',
  needs_exact_alarm: 'Allow "Alarms & reminders" for this app in Settings.',
  needs_notification_permission: 'Allow notifications for this app in Settings.',
  unavailable: 'Alarms work only in the Android app.',
};

export function setTimer(minutes: number): string {
  const s = alarmSupport();
  if (s !== 'ready') return HELP[s] ?? 'Alarm not available';
  const ok = scheduleAlarm('timer', {
    at: Date.now() + minutes * 60_000,
    title: 'Timer done',
    snoozeMinutes: 0,          // hides the Snooze button
    ringSeconds: 60,
  });
  return ok ? `Timer set for ${minutes} min` : 'Could not set the timer';
}

Reacciona a cómo terminó una alarma que sonaba y deténla desde tu propia pantalla:

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
  };
}

Notas: solo en la app de Android. repeat daily / weekly / weekdays / monthly / yearly mantienen la misma hora del reloj aunque cambie el horario de verano. Cancela con cancelNotification(id) en el puente; los resultados llegan a través de onAlarm.

appmintNative.ScreenOrientation appmintNative#

ScreenOrientation.lock({ orientation }) / ScreenOrientation.unlock()

Ejemplo

Fija la pantalla en vertical u horizontal para una vista, con la forma del plugin ScreenOrientation de Capacitor: el mismo cĂłdigo que lockOrientation().

Devuelve: Promise<void> en ambos. lock rechaza, como el de Capacitor, cuando orientation no es un tipo de bloqueo o la plataforma rechaza el bloqueo. Necesita: 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" />;
}

Bloquea para una sola pantalla solo cuando se permita un bloqueo, y deja que el usuario vuelva a girar:

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>;
}

Cuando necesites saber si el bloqueo funcionĂł, usa lockOrientation() directamente (responde false donde se rechazan los bloqueos, como en los navegadores de escritorio):

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');
}

Notas: orientation es uno de los tipos de bloqueo de Capacitor ('any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'portrait-secondary', 'landscape-primary', 'landscape-secondary') y se aplica tal cual. Los navegadores de escritorio rechazan los bloqueos; los navegadores mĂłviles suelen permitirlos solo a pantalla completa. lockOrientation(to) devuelve true/false. Da al usuario una salida de una vista bloqueada.

appmintNative.screenReader appmintNative#

screenReader.isEnabled() / screenReader.speak(text, lang?) / screenReader.onChange(cb)

Ejemplo

Le dice a la app si TalkBack está activado y anuncia mensajes cortos.

Devuelve: isEnabled() → Promise<boolean> (false en la web: no existe API web). speak() → Promise<void>. onChange(cb) → una función para cancelar la suscripción; cb(enabled) se ejecuta cuando se activa o desactiva el lector de pantalla (solo en la app). Requiere: 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');
}

Anuncia un resultado solo a quienes usan el lector de pantalla y deja los avisos visuales para los demás:

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

export async function notifySaved() {
  if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
  else toast('Note saved');
}

Quita una animación mientras TalkBack está activado y deja de escuchar cuando el componente desaparezca:

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>;
}

Notas: En la app speak pasa por el plugin ScreenReader; en la web (o si el plugin falla) se dice con texto a voz. Prefiere un buen marcado (regiones aria-label, aria-live) para la mayoría de los anuncios; usa speak para cambios cortos e importantes.

appmintNative.searchContacts appmintNative#

searchContacts(query: string, limit = 50): Promise<Contact[]>

Ejemplo

Busca contactos que coincidan con una palabra. Solo en la app.

Devuelve: Promise<Contact[]> - vacío cuando nada coincide, se rechaza el acceso o Contactos está desactivado. Necesita: lo mismo que listContacts: Contactos en el Paso 4 (Acceso) (activado por la compilación cuando tu código llama a esto) y el permiso del usuario la primera vez.

Cuadro de búsqueda con un pequeño retraso mientras se escribe.

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);
}

Muestra un estado claro cuando Contactos no está disponible y cuando nada coincide:

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>
  );
}

Busca a una persona por nombre y luego rellena de antemano un mensaje de texto para ella:

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
}

Notas: La coincidencia la hace la bĂşsqueda de contactos del telĂ©fono sobre los nombres. Cada contacto es { id?, name?, phones?: string[], emails?: string[], photoUri? }. En la web can('contactList') es 'none': no hay API web para leer la agenda. Los contactos son datos personales: decláralo en tu formulario de Seguridad de los datos de Play y en tu polĂtica de privacidad.

appmintNative.SecureKeyError appmintNative#

class SecureKeyError extends Error { code } — from '@/lib/appmintNative'

Ejemplo

El error que lanza secureKeys. Su code es una palabra corta con la que puedes bifurcar, igual en la app y en la web.

Devuelve: un Error con name === 'SecureKeyError' y code: 'no-such-key', 'auth-required' (desbloquea y reintenta), 'key-invalidated' (cambiaron los biomĂŠtricos o el bloqueo de pantalla: los datos se perdieron), 'strongbox-unavailable', 'disabled' (las claves seguras no estĂĄn en esta compilaciĂłn / no hay Web Crypto), 'bad-alias', 'bad-payload' (no es base64 vĂĄlido o es demasiado corto), 'too-large' (mĂĄs de 4 MB en una llamada en Android), 'foreign-origin' (lo pidiĂł una pĂĄgina de otro sitio), 'failed' (cualquier otro). Requiere: 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;
    }
  }
}

Convierte el código en un mensaje corto para el usuario en un solo 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 + ')';
  }
}

Trata una carga manipulada o incorrecta como un resultado normal y bórrala, pero conserva una clave ausente como un error que muestras:

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;
  }
}

Notas: e.message es code o "code: detail"; ramifica según e.code, nunca según el mensaje. Las palabras de error propias de las plataformas se convierten en estos códigos, y su texto original se conserva en el mensaje. Un contenido modificado o ajeno es 'bad-payload' en la web y 'failed' en la app, que no puede distinguirlo de otros errores de cifrado.

appmintNative.secureKeys appmintNative#

secureKeys — generate / encrypt / decrypt / remove / has / list / isHardwareBacked

Ejemplo

Claves de cifrado que el dispositivo guarda y la app nunca puede exportar: el Android Keystore en la app y una clave Web Crypto no extraĂ­ble en la web. Eliminar la clave es el Ăşnico "borrado seguro" real.

Devuelve: todas son Promises. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked}; encrypt(alias, text) → cadena base64; decrypt(alias, base64) → el texto; remove(alias) / has(alias) / isHardwareBacked(alias) → booleano; list() → string[]. generate, encrypt y decrypt lanzan SecureKeyError con un code. Requiere: Claves seguras (cifrado) en el Paso 4 (Acceso) al 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;
  }
}

Borra de verdad: nadie podrá leer los datos sellados después de esto:

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

export async function wipeDiary() {
  await secureKeys.remove('diary');
  localStorage.removeItem('diary');
}

Enumera las claves que guarda la app y muestra si cada una está protegida por hardware (la web siempre 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'}`));
}

Notas: can('secureKeys') es 'native' en la app y 'web' en un navegador, donde isHardwareBacked() siempre es false; nunca digas "hardware" ahí. Una clave requireAuth: en la app, encrypt/decrypt lanzan auth-required sin mostrar nada una vez pasada la ventana de desbloqueo; llama a unlock() e inténtalo de nuevo; en la web el runtime muestra unlock() por sí mismo. Cifra un registro cada vez (la app limita una llamada a 4 MB). Alias: letras, dígitos, _ - ., máximo 64.

appmintNative.sendSms appmintNative#

sendSms(phoneNumber: string, message: string): Promise<boolean>

Ejemplo

EnvĂa un SMS directamente, sin pantalla de confirmaciĂłn. Solo en la app de Android.

Devuelve: Promise<boolean> - true cuando el teléfono informa del mensaje como enviado, false cuando falló, cuando el SMS no estaba activado o cuando el usuario denegó el permiso. Requiere: el interruptor SMS (leer y enviar) en el Paso 4 (Acceso) (la compilación lo activa cuando tu código llama a esto) y el permiso del usuario en el primer uso.

Envía una alerta e informa al usuario de lo que pasó.

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';
  }
}

Pide primero una confirmación, ya que el mensaje se envía sin una pantalla para cancelarlo:

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.');
}

Envía la misma alerta a varias personas, una por una, y cuenta lo que salió:

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 };
}

Notas: El wrapper se resuelve con la PRIMERA respuesta, que es sent (el mensaje salió del teléfono), no delivered. Para informes de entrega usa el WebToApk.sendSms sin procesar con el evento appmint:sms. can('smsSend') es 'none' en la web (ningún navegador puede enviar un SMS). Cada SMS puede costarle dinero al usuario. El SMS es un permiso restringido de Google Play: necesitas una Declaración de permisos aprobada en Play Console, asà que prefiere composeSms() cuando el usuario pueda pulsar enviar.

appmintNative.setInAppPresentation appmintNative#

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

Ejemplo

Elige quién dibuja un mensaje dentro de la app (las campañas del creador): la app ('auto', el predeterminado: el diseño de banner, tarjeta, hoja o pantalla completa del creador) o tu propia interfaz ('none', mediante onInAppMessage).

Devuelve: boolean de forma síncrona: true cuando se guardó (permanece en el teléfono). Siempre false en la web y sin AppMint Push. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al compilar.

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

// The app draws its own messages from now on.
setInAppPresentation('none');

Un interruptor de ajustes:

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

export function onNativePopupsChange(on: boolean) {
  if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}

Toma el control mientras una pantalla está abierta y devuélvelo después:

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

export function useQuietScreen() {
  useEffect(() => {
    setInAppPresentation('none');
    return () => { setInAppPresentation('auto'); };
  }, []);
}

Notas: un mensaje entregado a tu código cuenta como mostrado. Establecer 'none' también cierra ahora uno que esté en pantalla.

appmintNative.setMusicVolume appmintNative#

setMusicVolume(percent) — @/lib/appmintNative

Ejemplo

Establece el nivel de la música de fondo en bucle de la app, de 0 a 100 (se redondea y se limita por ti).

Devuelve: void. Requiere: un archivo elegido en Música de fondo en el paso 3 (Integrar) al compilar. No hace nada cuando musicVolume() es -1.

La app conserva el nivel para el siguiente inicio, así que basta con un interruptor de silencio:

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

export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40);   // remembered by the app

Un control deslizante de volumen que aparece solo cuando la app incluye 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); }} />
  );
}

Baja la música mientras se reproduce un vídeo y devuélvela después:

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; }

Notas: 0 silencia la mĂşsica pero sigue reproduciĂŠndose. El nivel se guarda en el telĂŠfono (hasta que el usuario borre los datos de la app). Solo en la app de Android; en la web no pasa nada.

appmintNative.setPushPresentation appmintNative#

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

Ejemplo

Elige quiĂŠn dibuja un mensaje de AppMint Push enviado con estilo dentro de la app (banner, ventana emergente, hoja inferior o pantalla completa) mientras la app estĂĄ abierta: el shell de la app ('auto', el valor por defecto) o tu propio cĂłdigo ('none').

Devuelve: boolean de forma síncrona - true cuando se guardó el modo. Siempre false en la web. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al 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();
}

Comprueba la respuesta, ya que es false en la web o cuando AppMint Push estĂĄ desactivado:

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;
}

Dibuja tu propio banner para cada mensaje y mantén la bandeja 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); } };
}

Notas: la bandeja de entrada y el evento appmint:push funcionan igual en ambos modos. Establecer 'none' también cierra un mensaje que el shell esté mostrando ahora mismo.

appmintNative.setPushTag appmintNative#

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

Ejemplo

Establece una etiqueta de AppMint Push en esta instalación. El centro de envíos puede enviar a todos los que tengan una etiqueta, y una automatización de etiqueta puede enviar un mensaje un tiempo determinado después de establecerse la etiqueta. '' quita la etiqueta.

Devuelve: boolean de forma síncrona - true cuando se guardó; false cuando la clave no son 1-32 letras, dígitos o _, el valor supera los 128 caracteres o la instalación pasaría de 50 etiquetas. Siempre false en la web. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al compilar; la segmentación por etiquetas y las automatizaciones llegan a las apps compiladas a partir del 30 sep 2026.

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

// Abandoned cart: a tag automation on "cart" (say 60 minutes) reminds only phones whose cart is
// still open when the time is up. Setting 'open' again does not restart the clock.
export function useCartPushTag(itemCount: number) {
  const hasItems = itemCount > 0;
  useEffect(() => {
    setPushTag('cart', hasItems ? 'open' : '');
  }, [hasItems]);
}

export function onCheckoutComplete() {
  setPushTag('cart', '');
}

Comprueba el resultado: false significa que la etiqueta fue rechazada (clave no válida, valor de más de 128 caracteres, más de 50 etiquetas) o que estás en la web, así que lleva tu propio registro del 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;
}

Etiqueta un telĂ©fono cuando alcanza un hito, para que el centro de envĂos pueda dirigirse a todos los de ese nivel.

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]);
}

Notas: las etiquetas pertenecen a la instalación, no al usuario que inició sesión. No pongas nunca datos privados en una etiqueta.

appmintNative.setPushTags appmintNative#

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

Ejemplo

Establece varias etiquetas de AppMint Push en un solo cambio; un valor '' quita esa etiqueta, y las etiquetas que no nombres se conservan.

Devuelve: boolean de forma síncrona - true cuando se guardó; false cuando alguna clave o valor incumple las reglas de setPushTag (entonces no se guarda nada). Siempre false en la web. Necesita: Android: AppMint Push activado en el paso Integrar (Paso 3) al compilar; la segmentación por etiquetas y las automatizaciones llegan a las apps compiladas a partir del 30 sep 2026.

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

export function tagProfile(profile: { plan: 'free' | 'pro'; level: number; team?: string }) {
  return setPushTags({
    plan: profile.plan,
    level: String(profile.level),   // values are text
    team: profile.team ?? '',       // '' removes the tag
  });
}

// Sign-out: clear every tag and the user id.
export function signOutOfPush() {
  const cleared = Object.fromEntries(Object.keys(getPushTags()).map((k) => [k, '']));
  setPushTags(cleared);
  setPushUser('');
}

Etiqueta un teléfono con el idioma y los temas elegidos cuando el usuario guarde sus ajustes, y comprueba que se aceptaron todas las etiquetas.

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;
}

Mantén las etiquetas al día con la cuenta que inició sesión en un efecto de React, quitando las que ya no apliquen.

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]);
}

Notas: se guarda todo lo de una llamada o nada.

appmintNative.setPushUser appmintNative#

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

Ejemplo

Indica a AppMint Push cuĂĄl de tus usuarios ha iniciado sesiĂłn en esta instalaciĂłn, para que el centro de envĂos pueda enviar un mensaje a ese usuario. Pasa tu propio id de cuenta estable tras iniciar sesiĂłn y '' al cerrar sesiĂłn.

Devuelve: boolean de forma síncrona - true cuando se guardó el id; false para un id con algo distinto de letras, dígitos y _ @ . : + - (hasta 128). Siempre false en la web. Requiere: Android: AppMint Push activado en el paso Integrar (paso 3) al compilar; el envío a un usuario llega a las apps compiladas a partir del 30 sep 2026.

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

// Keeps AppMint Push in step with the signed-in account: the auth user id, never the email.
export function usePushUser() {
  useEffect(() => {
    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const id = session?.user.id ?? '';
      if (getPushUser() !== id) setPushUser(id);
    });
    return () => data.subscription.unsubscribe();
  }, []);
}

Gestiona un id rechazado: setPushUser responde false para una dirección de correo o cualquier id con caracteres fuera del conjunto permitido, así que envía en su lugar un id estable.

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;
}

Borra el id cuando el usuario cierre sesión, en un controlador de clic normal.

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>
  );
}

CombĂnalo con una etiqueta para que un solo paso de inicio de sesiĂłn identifique al usuario y su plan a la vez.

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

export function onSignedIn(userId: string, plan: 'free' | 'pro') {
  setPushUser(userId);
  setPushTag('plan', plan);
}

Notas: el id pertenece a la instalación, así que al cerrar sesión hay que borrarlo. No borra las etiquetas: llama a setPushTags con valores '' para las específicas del usuario.

appmintNative.setStatusBar appmintNative#

setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void

Ejemplo

Pinta la barra de estado del sistema para que combine con la pantalla y elige iconos claros u oscuros para ella.

Devuelve: nada. Requiere: nada.

style es el color de los ICONOS: 'light' = iconos claros para una barra oscura (el valor por defecto), 'dark' = iconos oscuros para una barra clara. Llámalo una vez al iniciar con el --bg de la app, y de nuevo en cualquier pantalla con un fondo distinto:

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

export function App() {
  useEffect(() => { setStatusBar('#0B0F1A', 'light'); }, []);   // dark app, light icons
  return <main>{/* … */}</main>;
}

export function PhotoViewer() {
  useEffect(() => {
    setStatusBar('#000000', 'light');
    return () => setStatusBar('#0B0F1A', 'light');                // restore on leave
  }, []);
  return null;
}

Una pantalla clara:

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

setStatusBar('#FFFFFF', 'dark');

La forma de Capacitor (el estilo lleva el nombre del FONDO: 'DARK' = iconos claros):

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

await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });

Notas:

  • En la app de Android el shell pinta la barra él mismo, lo que también funciona en Android 15 de borde a borde; un color que no se pueda interpretar se ignora: pasa #RRGGBB. Una llamada solo de estilo (backgroundColor = null) conserva el color que esta página estableció por última vez y cambia los iconos; antes de que la página establezca algún color cambia los iconos mediante el plugin StatusBar y la barra mantiene el color elegido al compilar.
  • En la web establece <meta name="theme-color">; el navegador elige por sí mismo el color de los iconos.

appmintNative.setVolume appmintNative#

setVolume(stream, percent, showUi?) — @/lib/appmintNative

Ejemplo

Establece el nivel de volumen propio del teléfono para un flujo, de 0 a 100. showUi muestra el control deslizante de volumen del teléfono mientras se mueve.

Devuelve: { ok, stream?, current?, max?, percent?, error? } - el error es unknown_stream, bad_percent, fixed_volume, dnd_access_required (No molestar bloquea el timbre / las notificaciones: llama a requestDndAccess()), muted (el modo "Silencio total" de No molestar silencia la secuencia, asĂ que el nivel no cambiĂł), not_applied, foreign_origin, failed, o unavailable en la web. Requiere: nada para multimedia, alarma y llamada.

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();
      }}
    />
  );
}

Muestra el volumen multimedia como un control deslizante que sigue las 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))} />;
}

Sube el volumen de la alarma antes de que suene una pantalla de alarma, e informa de por quĂŠ no cambiĂł.

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>

Ejemplo

Abre la hoja de compartir del sistema en la app y la Web Share API en un navegador.

Devuelve: Promise<boolean> - true cuando se abrió la hoja; false cuando el usuario la cerró o compartir no está disponible. Se rechaza cuando lo compartido en sí no es válido (por ejemplo una url no admitida) o cuando otra hoja de compartir sigue abierta. Requiere: 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>
  );
}

Solo texto (sin enlace) tambiĂŠn sirve:

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

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

La forma al estilo Capacitor Share.share(opts) llama al mismo código pero RECHAZA cuando el usuario cancela (el contrato de Capacitor) y resuelve { activityType: '' } en caso contrario; Share.canShare() resuelve { 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 */
  }
}

Notas:

  • Pasa solo los campos que tengas. En la app, el texto y los enlaces pasan por el plugin Share de Capacitor.
  • files funciona en la app de Android (los archivos van a la hoja de compartir como adjuntos reales; se añade un url al texto) y en la web donde el navegador puede compartir archivos (navigator.canShare({ files })). MantĂ©n los archivos pequeños: se pasan a la app en memoria.
  • En la web, llĂĄmalo desde un toque; los navegadores rechazan un compartir que no iniciĂł una acciĂłn del usuario.

appmintNative.Share appmintNative#

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

Ejemplo

Documentado junto con appmintNative.share: el ejemplo de allí también muestra este.

Abre la hoja de compartir del sistema en la app y la Web Share API en un navegador.

Devuelve: Promise<boolean> - true cuando se abrió la hoja; false cuando el usuario la cerró o compartir no está disponible. Se rechaza cuando lo compartido en sí no es válido (por ejemplo una url no admitida) o cuando otra hoja de compartir sigue abierta. Requiere: 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>
  );
}

Solo texto (sin enlace) tambiĂŠn sirve:

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

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

La forma al estilo Capacitor Share.share(opts) llama al mismo código pero RECHAZA cuando el usuario cancela (el contrato de Capacitor) y resuelve { activityType: '' } en caso contrario; Share.canShare() resuelve { 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 */
  }
}

Notas:

  • Pasa solo los campos que tengas. En la app, el texto y los enlaces pasan por el plugin Share de Capacitor.
  • files funciona en la app de Android (los archivos van a la hoja de compartir como adjuntos reales; se añade un url al texto) y en la web donde el navegador puede compartir archivos (navigator.canShare({ files })). MantĂ©n los archivos pequeños: se pasan a la app en memoria.
  • En la web, llĂĄmalo desde un toque; los navegadores rechazan un compartir que no iniciĂł una acciĂłn del usuario.

appmintNative.sharedText appmintNative#

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

Ejemplo

Entrega el texto o el enlace que otra app compartió HACIA esta app ("Compartir → tu app"), por ejemplo un enlace compartido desde un navegador o YouTube.

Devuelve: de forma síncrona, { text, subject, url, receivedAt } o null cuando no hay nada esperando. url es el primer enlace http(s) encontrado en text ('' si no hay ninguno); receivedAt son milisegundos desde 1970. Necesita: la opción Recibir contenido de otras apps de la compilación con "Texto y enlaces"; se activa automáticamente cuando tu código llama a sharedText( o onShared(.

Comprueba al montar, guárdalo y luego bórralo:

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]);
}

Escucha un contenido compartido que llega con la app ya abierta mediante onShared y bórralo después de usarlo.

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(''); } };
}

Rellena un formulario desde un enlace compartido y no muestres nada en la web, donde sharedText() es 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" />;
}

Notas:

  • El contenido compartido se CONSERVA hasta que llames a clearShared(), así que una app que se monta después de recibirlo aún lo ve. Bórralo tras guardarlo, o el siguiente montaje lo guardará otra vez.
  • Para un contenido compartido que llega con la app abierta, usa onShared().
  • Una foto o vĂdeo compartido CON un pie de texto: el pie llega aquĂ, el archivo por la API de archivo abierto.
  • Solo en la app de Android. can('shareTarget') es 'none' en la web, y sharedText() devuelve null allĂ.

appmintNative.sheet appmintNative#

sheet(opts: { title?, content, dismissible? }): { close(), element, closed }

Ejemplo

Abre una hoja modal inferior con comportamiento real de hoja: para un panel de filtros, un editor o una vista previa de detalle.

Devuelve: un handle: close() la cierra, element es el elemento del cuerpo (rellĂŠnalo o actualĂzalo), closed es una Promise<void> que se resuelve cuando la hoja se ha cerrado, sea cual sea la forma. Requiere: nada.

Opciones: title (encabezado opcional), content: un elemento, texto plano (no HTML) o una funciĂłn (host) => { … } que rellena el cuerpo; dismissible (por defecto true: tocar el fondo, arrastrar hacia abajo, Escape y el gesto de retroceso la cierran).

Contenido DOM simple:

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;

Contenido de React, renderizado en la hoja con un portal:

import { useState } from 'react';
import { createPortal } from 'react-dom';
import { sheet } from '@/lib/appmintNative';

export function FiltersButton() {
  const [host, setHost] = useState<HTMLElement | null>(null);
  const [close, setClose] = useState<() => void>(() => () => {});

  const open = () => {
    const h = sheet({ title: 'Filters', content: () => {} });
    setHost(h.element);
    setClose(() => h.close);
    void h.closed.then(() => setHost(null));      // unmount the portal after any close
  };

  return (
    <>
      <button onClick={open}>Filters</button>
      {host && createPortal(
        <form onSubmit={(e) => { e.preventDefault(); close(); }}>
          <label><input type="checkbox" name="open" /> Open now</label>
          <button type="submit">Apply</button>
        </form>,
        host,
      )}
    </>
  );
}

Una hoja que permanece abierta hasta que el usuario elige, usando dismissible: false y la promesa 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;
}

Notas:

  • Dibujado con los tokens de la app (--surface, --ink, --muted, --border), sobre el ĂĄrea segura, con un toque hĂĄptico ligero al abrirse.
  • Arrastrar hacia abajo la cierra cuando se arrastra mĂĄs de 120 px o se desliza con impulso; el cuerpo se desplaza primero cuando no estĂĄ arriba del todo.
  • Con dismissible: false solo tu propio close() lo cierra: dale un botĂłn al usuario.
  • Para una simple lista de acciones usa actionSheet().

appmintNative.shortcuts appmintNative#

shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()

Ejemplo

Añade accesos directos al menú de pulsación larga del icono de la app (accesos directos dinámicos de Android). Un toque abre la app en esa ruta.

Devuelve: set() devuelve Promise<boolean> - true cuando los atajos se entregaron al sistema, false donde no hay (la web). clear() devuelve Promise<void>. Necesita: nada.

Defínelos una vez al inicio y enruta según el enlace de apertura:

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

export function useAppShortcuts() {
  const navigate = useNavigate();
  useEffect(() => {
    if (can('shortcuts') !== 'none') {
      void shortcuts.set([
        { id: 'new', title: 'New note', route: '/new' },
        { id: 'search', title: 'Search', route: '/search' },
      ]);
    }
    void launchUrl().then((u) => { if (u) navigate(new URL(u).pathname); });
  }, [navigate]);
}

Elimínalas (por ejemplo tras cerrar sesión):

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

await shortcuts.clear();

Abre la pantalla correspondiente cuando se toca un acceso directo con la app ya en ejecución, con onDeepLink.

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

export function useShortcutRouting() {
  const navigate = useNavigate();
  useEffect(() => onDeepLink((url) => navigate(new URL(url).pathname)), [navigate]);
}

Muestra atajos solo para un usuario con sesiĂłn iniciada y gestiona la web, donde 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]);
}

Notas:

  • Como máximo 4 elementos; los sobrantes se descartan. Un route sin / inicial recibe uno. id es opcional.
  • Android 7.1+; en versiones anteriores de Android la llamada no hace nada. Los tĂtulos se recortan a 25 caracteres (etiqueta corta) y 50 (etiqueta larga).
  • Un toque abre la app con el enlace de lanzamiento appmint-shortcut://<package><route>, asĂ que new URL(u).pathname es tu ruta. LĂ©elo con launchUrl() al montar y onDeepLink() mientras la app se ejecuta.
  • can('shortcuts') es 'none' en la web: un navegador no tiene icono de app.

appmintNative.snoozeAlarm appmintNative#

snoozeAlarm(id?, minutes?) — @/lib/appmintNative

Ejemplo

Detiene la alarma que suena y la hace sonar de nuevo tras minutes (0 u omitido = el snoozeMinutes propio de la alarma, por defecto 10). El timbre de mañana de una alarma repetitiva no se mueve.

Devuelve: boolean - true cuando estaba sonando y queda pospuesta. Requiere: 🔔 Despertador en el paso Acceso (paso 4) al 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);
});

Pospón desde un botón en tu propia pantalla de alarma, usando onAlarm para saber cuándo está sonando.

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 cuando nada está sonando, así que comprueba antes de prometer al usuario un segundo timbre.

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

Ejemplo

Lee texto en voz alta con el motor de texto a voz del teléfono.

Devuelve: void - empieza a hablar (o se pone en cola tras el habla actual) y no da señal de "terminado". opts: { lang?: string; rate?: number; pitch?: number } (por defecto: idioma del motor, 1, 1). Necesita: 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>
  );
}

Distintos idiomas y velocidades:

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

Di una breve confirmación tras una acción, y díselo también al usuario en texto.

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

export function announceSaved(title: string) {
  toast('Saved');
  speak(`${title} saved`, { rate: 1.1 });
}

Lee una lista elemento tras elemento; speak() encola, así que llámalo en un bucle y detén todo con stopSpeaking() cuando se cierre la pantalla.

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>;
}

Notas: En la app de Android: el motor nativo de la app. En la web: speechSynthesis. Varias llamadas a speak() se reproducen una tras otra. Si necesitas saber cuĂĄndo termina la voz (para resaltar palabras o continuar), usa window.speechSynthesis con el onend de una locuciĂłn directamente; tambiĂŠn funciona en la app de Android.

appmintNative.speech appmintNative#

speech — @/lib/appmintNative

Ejemplo

Dictado: convierte la voz del usuario en texto. El reconocedor de voz de Android en la app de Android, la Web Speech API en Chrome.

Devuelve: speech.isAvailable() → Promise<boolean>. speech.start(onResult, opts?) → Promise<boolean>: true cuando empezĂł a escuchar, false cuando se denegĂł el permiso del micrĂłfono o el dictado no estĂĄ disponible. onResult(text, isFinal) se llama con el texto. speech.stop() → Promise<void>. opts: { lang?: string; partial?: boolean; prompt?: string }. Requiere: MicrĂłfono en el Paso 4 (Acceso); las compilaciones con IA lo activan cuando el cĂłdigo usa reconocimiento 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>
  );
}

Dicta en un campo de texto e indica cuando se rechazó el micrófono, ya 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>
  );
}

Deja de escuchar cuando se cierre la pantalla y lee el resultado con speak.

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

export function VoiceCommand() {
  const [heard, setHeard] = useState('');
  useEffect(() => () => { void speech.stop(); }, []);

  return (
    <button
      onClick={() => void speech.start((t, isFinal) => {
        setHeard(t);
        if (isFinal) speak(`You said ${t}`);
      }, { lang: 'en-US' })}
    >
      {heard || 'Say something'}
    </button>
  );
}

Notas:

  • Con partial activado (el predeterminado) onResult(text, false) llega a medida que entran las palabras y luego una vez onResult(text, true) con el texto final. Con partial: false solo recibes el final, cuando el usuario deja de hablar.
  • El aviso de permiso se muestra en la primera start().
  • Si el usuario no dice nada, onResult no se llama; la escucha simplemente termina. Deja un botón Detener o un tiempo límite en tu interfaz.
  • La escucha se detiene sola tras una pausa. lang usa por defecto el idioma del teléfono.

appmintNative.splash appmintNative#

splash.hold() / splash.ready()

Ejemplo

Mantiene la pantalla de presentación del inicio hasta que la primera pantalla real esté lista, en lugar de mostrar "presentación y luego un indicador de carga".

Devuelve: nada (ambos son síncronos). Necesita: nada.

Por defecto la pantalla de presentación se oculta sola en el primer pintado de la app: no necesitas nada. Cuando la primera pantalla necesita datos, mantén la presentación al inicio del módulo y libérala cuando esa pantalla esté visible:

// 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>;
}

Mantén la pantalla de presentación mientras se restaura una sesión iniciada y libérala en un finally para que un fallo no la deje puesta.

// main.tsx
import { splash } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';

splash.hold();
supabase.auth.getSession().finally(() => splash.ready());

Libéralo una sola vez, desde un hook, cuando los datos de la primera pantalla ya estén visibles.

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]);
}

Notas:

  • En la app de Android hold() le dice al shell que mantenga su pantalla de bienvenida; ready() la oculta (el tiempo mínimo de bienvenida del creador sigue aplicándose).
  • En la web ambos no hacen nada: no hay pantalla de bienvenida.
  • El shell limita la espera a unos 6 segundos, asĂ que una pĂĄgina que nunca llama a ready() igualmente se abre.

appmintNative.StatusBar appmintNative#

StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })

Ejemplo

La barra de estado con la forma del plugin StatusBar de Capacitor, construida sobre setStatusBar().

Devuelve: Promise<void> en ambos. Requiere: 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' });

Color e iconos juntos: prefiere setStatusBar(), que establece ambos en una sola llamada:

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

setStatusBar('#0B0F1A', 'light');   // dark bar, light icons
setStatusBar('#FFFFFF', 'dark');    // light bar, dark icons

Cambia la barra con el tema claro y oscuro de la propia app, usando juntas las llamadas al estilo Capacitor.

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

export function useStatusBarForTheme(dark: boolean) {
  useEffect(() => {
    void StatusBar.setBackgroundColor({ color: dark ? '#0B0F1A' : '#FFFFFF' });
    void StatusBar.setStyle({ style: dark ? 'DARK' : 'LIGHT' });   // 'DARK' = dark bar, light icons
  }, [dark]);
}

Notas: style sigue a Capacitor: 'DARK' significa fondo oscuro (iconos claros), cualquier otro valor significa iconos oscuros. En la app de Android el puente del shell pinta la barra (la llamada de color del plugin no hace nada en Android 15+), y setStyle() invierte los iconos según el color actual de la barra. En la web el color pasa a ser <meta name="theme-color">.

appmintNative.stopAlarm appmintNative#

stopAlarm(id?) — @/lib/appmintNative

Ejemplo

Detiene la alarma que está sonando, como su botón Descartar. Una alarma repetitiva sigue programada para su próxima vez.

Devuelve: boolean - true cuando estaba sonando y ahora está detenida. id omitido = la que esté sonando. Requiere: 🔔 Despertador en el paso Acceso (paso 4) al 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>
  );
}

Detén la alarma desde la pantalla que abre la app cuando suena, y reacciona a todas las formas en que termina:

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 ?? ''); },
  };
}

Comprueba la respuesta: false significa que nada estaba sonando (o que estás en la web), así que no afirmes que se detuvo:

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

Ejemplo

Detiene ahora el texto a voz y descarta todo lo que aún esté esperando para hablarse.

Devuelve: void. Necesita: 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>
  );
}

Lee en voz alta un texto largo con el idioma y la velocidad que eligió el usuario, y deténlo desde un botón:

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>
  );
}

Corta la voz cuando el usuario empiece a escribir una respuesta:

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

export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
  return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}

Notas: Es seguro llamarlo cuando no se está hablando. En la app de Android: la detención del motor nativo. En la web: speechSynthesis.cancel().

appmintNative.swipeable appmintNative#

swipeable(el: HTMLElement, opts: SwipeOptions): () => void

Ejemplo

Añade gestos de deslizamiento a un elemento: filas que se borran al deslizar, paginación, descartar una tarjeta.

Devuelve: una función para cancelar la suscripción que además restaura el touch-action original del elemento. Requiere: nada.

Opciones: onLeft, onRight, onUp, onDown (al menos una), threshold (px de recorrido, por defecto 56), edgeGuard (px desde el borde izquierdo/derecho de la pantalla donde se ignoran los deslizamientos horizontales, por defecto 24), haptics (toque hĂĄptico de selecciĂłn al deslizar, por defecto true).

Desliza a la izquierda para eliminar, con un botĂłn visible para la misma acciĂłn:

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>
  );
}

Pasa entre pestañas con izquierda / derecha:

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();

Descarta una tarjeta deslizándola hacia arriba, con el manejador eliminado al 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>;
}

Notas:

  • Basado en Pointer Events, asĂ que el mismo cĂłdigo funciona con tacto, lĂĄpiz y arrastre con ratĂłn.
  • Establece touch-action por ti: pan-y para manejadores horizontales (el desplazamiento vertical sigue funcionando), pan-x para los verticales, none para ambos.
  • Un deslizamiento debe ser claramente en una dirección (1,5 veces más en un eje); los desplazamientos en diagonal no se activan. Un segundo dedo cancela el gesto.
  • La guarda de borde existe porque el gesto de retroceso del sistema Android controla los bordes de la pantalla.

appmintNative.systemPalette appmintNative#

systemPalette(): Promise<SystemPalette>

Ejemplo

Lee los colores Material You del teléfono (creados a partir del fondo de pantalla en Android 12+), para que el color de acento de la app pueda seguir el fondo del usuario como las apps del sistema.

Devuelve: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - cada muestra asigna un tono ('0', '10', '50', '100' … '900', '1000') a un color hexadecimal. { available: false } por debajo de Android 12 y en la web. Necesita: 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']);
}

Usa los colores del fondo de pantalla solo cuando el teléfono los ofrezca y vuelve a leerlos cuando la app regrese a la pantalla:

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)
}

Muestra el interruptor "Igualar mi fondo de pantalla" solo cuando exista una paleta, usando { available: false } como respuesta a "aquí no":

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>;
}

Notas: Comprueba el contraste antes de usar un tono del fondo de pantalla para texto o botones: --accent debe seguir alcanzando 3:1 sobre tu fondo. Ofrécelo como opción ("Igualar con mi fondo de pantalla"), no como tema forzado. Llåmalo de nuevo cuando la app se reanude; el usuario puede haber cambiado el fondo.

appmintNative.TextToSpeech appmintNative#

TextToSpeech — @/lib/appmintNative

Ejemplo

El objeto TextToSpeech al estilo de Capacitor (speak / stop), que se ejecuta con el mismo código que speak() y stopSpeaking().

Devuelve: speak({ text, lang?, rate?, pitch? }) → Promise<void> que resuelve cuando el texto ha sido leído (o stop() lo cortó) y rechaza cuando no pudo leerse. stop() → Promise<void>. Necesita: 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();
}

Lee en voz alta una lista de líneas una tras otra; cada speak se resuelve cuando se encola, así que el motor las reproduce en orden:

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

export async function readSteps(steps: string[]) {
  await TextToSpeech.stop();   // drop anything still waiting from before
  for (const [i, s] of steps.entries()) {
    await TextToSpeech.speak({ text: `Step ${i + 1}. ${s}`, lang: 'en-US' });
  }
}

Un botón de lectura en voz alta que deja de hablar al salir de la pantalla:

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>;
}

Notas: await TextToSpeech.speak(…) es la señal de fin: el paso siguiente tras hablar puede seguirla directamente. En la app: el motor nativo de texto a voz de Android del shell; en la web: speechSynthesis.

appmintNative.textZoom appmintNative#

textZoom.get() / textZoom.getPreferred() / textZoom.set(value)

Ejemplo

Un control de tamaño de texto dentro de la app. En la app cambia el zoom del texto de la página (1 = 100%); en la web cambia el tamaño de fuente raíz, que escala todo diseño basado en rem.

Devuelve: get() → Promise<number>. getPreferred() → Promise<number>: el tamaño que el usuario eligiĂł en los ajustes del telĂ©fono (1 en la web). set(value) → Promise<void>; el valor se limita a 0.5-3 y se rechaza si no es un nĂşmero. Requiere: 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()); }

Recuerda la elección entre inicios:

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

const saved = Number(localStorage.getItem('textZoom'));
if (saved) await textZoom.set(saved);

export async function setSize(v: number) {
  await textZoom.set(v);
  localStorage.setItem('textZoom', String(v));
}

Un selector de tamaño de texto que lee el valor actual, lo mantiene dentro del intervalo permitido y lo muestra en porcentaje:

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>
  );
}

Notas: La app ya sigue el ajuste de tamaño de fuente del telĂ©fono; este es un control adicional dentro de la app. Prueba el diseño con tamaños grandes: las alturas fijas en controles de texto recortan el contenido.

appmintNative.toast appmintNative#

toast(message: string, opts?: { duration?: 'short' | 'long' }): void

Ejemplo

Muestra un mensaje breve que no necesita respuesta: "Copiado", "Guardado", "Añadido a la lista".

Devuelve: nada (lanzar y olvidar). Necesita: 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>
  );
}

Un mensaje más largo permanece unos 3,5 segundos:

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

toast('Your export will be ready in a minute', { duration: 'long' });

La forma de Capacitor Toast.show({ text, duration? }) hace lo mismo y resuelve Promise<void>:

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

await Toast.show({ text: 'Added to list' });

Para un mensaje que el usuario DEBE ver, usa un diálogo:

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

Notas:

  • En la app de Android, el plugin Toast de Capacitor lo muestra en la parte inferior de la pantalla.
  • En cualquier otro lugar es una pequeña snackbar dibujada sobre la pĂĄgina con los colores propios de la app (--ink sobre --bg), por encima del margen del ĂĄrea segura, con role="status" para que los lectores de pantalla la anuncien.
  • Solo se muestra uno a la vez; un nuevo aviso reemplaza al anterior.
  • Un toast es fĂĄcil de pasar por alto por diseño. Nunca lo uses para un error que el usuario tenga que atender.

appmintNative.Toast appmintNative#

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

Ejemplo

Documentado junto con appmintNative.toast: el ejemplo de allí también muestra este.

Muestra un mensaje breve que no necesita respuesta: "Copiado", "Guardado", "Añadido a la lista".

Devuelve: nada (lanzar y olvidar). Necesita: 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>
  );
}

Un mensaje más largo permanece unos 3,5 segundos:

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

toast('Your export will be ready in a minute', { duration: 'long' });

La forma de Capacitor Toast.show({ text, duration? }) hace lo mismo y resuelve Promise<void>:

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

await Toast.show({ text: 'Added to list' });

Para un mensaje que el usuario DEBE ver, usa un diálogo:

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

Notas:

  • En la app de Android, el plugin Toast de Capacitor lo muestra en la parte inferior de la pantalla.
  • En cualquier otro lugar es una pequeña snackbar dibujada sobre la pĂĄgina con los colores propios de la app (--ink sobre --bg), por encima del margen del ĂĄrea segura, con role="status" para que los lectores de pantalla la anuncien.
  • Solo se muestra uno a la vez; un nuevo aviso reemplaza al anterior.
  • Un toast es fĂĄcil de pasar por alto por diseño. Nunca lo uses para un error que el usuario tenga que atender.

appmintNative.torch appmintNative#

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

Ejemplo

La linterna del teléfono: torch.on(), off(), toggle(), isOn(), isAvailable(). can('torch') es 'none' en la web.

Devuelve: on()/off() → Promise (se rechaza con code 'unavailable' o 'CAMERA_IN_USE'); toggle() → el nuevo estado; isOn() / isAvailable() → booleano. Requiere: nada, ningĂşn permiso.

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>
  );
}

Apaga la luz cuando el usuario salga de la pantalla y muestra el error que informa el teléfono cuando no se puede cambiar:

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>;
}

Haz parpadear la luz como señal y luego déjala 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));
  }
}

Notas: se apaga cuando la app se cierra. Mientras el flujo de la cámara propio de la app esté abierto, enciéndela con la restricción torch de la pista.

appmintNative.trackEvent appmintNative#

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

Ejemplo

Informa de un momento de tu app para que se muestren los mensajes dentro de la app del creador con el disparador "Cuando ocurre un evento" (mismo nombre). La app descarga las campañas del creador desde el centro de envĂos de AppMint Push y las muestra ella misma.

Devuelve: boolean de forma síncrona: false para un nombre no válido (letras, dígitos y _ . : -, hasta 64), en la web y en apps compiladas sin AppMint Push. Necesita: Android: AppMint Push activado en el paso Integrar (Paso 3) al 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
}

Informa de un cambio de vista que la URL no muestra, para que los mensajes "En una página" lo vean:

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

export function selectTab(tab: 'home' | 'cart' | 'profile') {
  setActiveTab(tab);
  trackEvent('appmint:route', { path: '/' + tab });
}

Un hook que registra la primera vez que se abre una pantalla:

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]);
}

Notas: los cambios de URL (cargas de página, pushState, rutas #hash) ya se detectan; informa solo de lo que la dirección no cambia.

appmintNative.transition appmintNative#

transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>

Ejemplo

Ejecuta un cambio de pantalla con una animación de aspecto nativo: push desliza la nueva pantalla desde la derecha, pop desliza hacia atrás, fade hace un fundido cruzado.

Devuelve: Promise<void> que se resuelve tras el cambio y su animaciĂłn. update es tu cambio de estado (un navigate del router, un setState) y siempre se aplica. Requiere: 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>;
}

Fundido entre dos pestañas:

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

declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });

Usa un fundido para el cambio de pestaña, en un controlador por pestaña:

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>
  );
}

Notas:

  • Usa la View Transitions API cuando el WebView la tiene. Si no, aplica la actualización y reproduce una breve animación de entrada en #root (o en el primer hijo de <body>).
  • Cuando el usuario pidió al sistema reducir el movimiento, solo aplica la actualización, sin animación.
  • Las animaciones duran unos 240 ms.

appmintNative.unlock appmintNative#

unlock(title?, subtitle?) — from '@/lib/appmintNative'

Ejemplo

Pide al dueño del telĂ©fono que demuestre que estĂĄ presente (huella, rostro o PIN/cĂłdigo del dispositivo) y te dice si lo hizo.

Devuelve: Promise<boolean> - true cuando se desbloquea; false cuando se cancela, falla, hay bloqueo por intentos o nada puede comprobar en este dispositivo. Nunca lanza error. Requiere: en la app, Huella digital en el Paso 4 (Acceso) al compilar. En la web: el autenticador de plataforma del 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();
}

Oculta el botón donde no haya nada con lo que desbloquear:

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

const showLockButton = can('unlock') !== 'none';   // 'native' | 'web' | 'none'

Protege un borrado con un desbloqueo reciente y luego confirma con un toque hĂĄptico y un toast:

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

export async function deleteAccountData(wipe: () => Promise<void>) {
  const ok = await unlock('Confirm delete', 'This removes your saved data');
  if (!ok) { haptic('warning'); toast('Not unlocked, nothing was deleted'); return; }
  await wipe();
  haptic('success');
  toast('Data deleted');
}

Notas: En la app se permite el PIN, patrón o contraseña del dispositivo como segunda forma de entrada. En la web, la primera llamada registra el autenticador de plataforma del navegador y cuenta como desbloqueado. Esto demuestra que el dueño del dispositivo está presente; NO es un inicio de sesión: nunca lo uses como única protección de datos que viven en un servidor. En Android 11 y posteriores el aviso se abre en cualquier teléfono con bloqueo de pantalla (uno solo con PIN también lo recibe y se desbloquea con el PIN); en Android 10 y anteriores primero debe haber una huella guardada; si no, responde false sin aviso.

appmintNative.updates appmintNative#

updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()

Ejemplo

Un actualizador del lado de la página para las actualizaciones del paquete web. El código de la página no lo necesita: en una app de AppMint, la actualización remota la hace de forma nativa la app de Android, y updates.supported() devuelve false en todas partes.

Devuelve: supported() → boolean (actualmente siempre false); download(url, sha256, version, binaryVersion) → Promise<boolean> (false mientras no sea compatible); stage(dir, version, binaryVersion) → Promise<boolean>; confirm() → void; revertIfUnconfirmed(binaryVersion) → Promise<boolean>; current() → { path, version, binaryVersion } | null. Requiere: nada para tu app: las actualizaciones se publican desde Remote Update en AppMint.

El Ăşnico uso seguro en el cĂłdigo de la app es comprobarlo y no hacer nada cuando estĂĄ desactivado:

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);
}

Muestra qué paquete se está ejecutando (una fila "Acerca de" en ajustes):

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';

Muestra la versiĂłn del contenido en ejecuciĂłn en una fila de ajustes, con la etiqueta integrada cuando la pĂĄgina no tiene un paquete 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>;
}

Notas:

  • En la app de Android, el gestor de actualización remota del shell descarga, verifica y sirve las actualizaciones, y revierte un paquete que no se carga. La página la sirve el propio shell, así que un paquete preparado desde el código de la página nunca se mostraría: por eso supported() es false.
  • En la web no hay nada que actualizar; la pĂĄgina es lo que envĂa el servidor.
  • Las actualizaciones solo pueden cambiar HTML, CSS y JavaScript; una nueva función nativa sigue necesitando una nueva compilación de la app.

appmintNative.watchIdle appmintNative#

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

Ejemplo

Avisa a la app cuando el usuario queda inactivo o bloquea la pantalla, y cuando vuelve (can('idle')). InĂcialo desde un toque la primera vez: la solicitud de permiso lo necesita.

Devuelve: Promise de la funciĂłn de parada. El callback recibe { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } al inicio y en cada cambio. Se rechaza con code 'permission-denied' o 'unavailable'. Requiere: nada, ningĂşn permiso de 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>
  );
}

Muestra un estado de ausencia y deja de vigilar al desmontar; un permiso denegado se informa con su 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 };
}

Pausa un temporizador de sondeo mientras el usuario estĂĄ ausente y actualiza cuando vuelve:

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); };
}

Notas: el umbral es de al menos un minuto (el mínimo de la especificación).

appmintNative.widget appmintNative#

widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()

Ejemplo

Rellena el widget de la pantalla de inicio de la app: una tarjeta con tĂtulo, un valor grande, un pie, hasta doce lĂneas (cada una con color, marcador y ruta opcionales) y un toque que abre la app en una ruta.

Devuelve: set(card) → Promise<boolean> (true cuando se guardĂł); clear() → Promise<void>; placedCount() → Promise<number> (cuĂĄntas copias colocĂł el usuario); requestPin() → Promise<boolean> (true cuando se abriĂł el flujo "añadir widget" del launcher). Ninguno se rechaza. Requiere: nada en la app de Android (el widget siempre forma parte de la compilaciĂłn).

Actualiza la tarjeta después de cada cambio que el usuario querría ver en la pantalla de inicio:

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
  });
}

Una agenda que llena un widget redimensionado, con fondo transparente y una línea de retraso en rojo:

import { widget, type WidgetLine } from '@/lib/appmintNative';

export async function updateAgendaWidget(events: { day: string; time: string; title: string; overdue: boolean }[]) {
  const lines: WidgetLine[] = events.slice(0, 12).map((e) => e.overdue
    ? { text: `Overdue: ${e.title}`, color: '#FF5252', marker: '#FF5252', bold: true, route: `/day/${e.day}` }
    : { text: `${e.time} ${e.title}`, marker: '#29B6F6', route: `/day/${e.day}` });
  await widget.set({
    title: 'Agenda', value: 'Today', caption: `${events.length} events`,
    bg: 'rgba(20, 38, 74, 0.6)', textSize: 'auto', lines, route: '/agenda',
  });
}

Un botĂłn "Añadir a la pantalla de inicio":

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>
  );
}

Vacía la tarjeta cuando el usuario cierre sesión y muestra cuántas copias hay en la pantalla de inicio:

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>;
}

Notas:

  • La tarjeta se guarda en el dispositivo, sobrevive a un reinicio y cada copia colocada se actualiza. placedCount() de 0 significa que aĂşn no hay ninguna copia en la pantalla de inicio.
  • Campos de la tarjeta: title (vacío = el nombre de la app), value (se muestra grande en accent, oculto cuando está vacío), caption (texto pequeño bajo el valor), lines (máx. 12; seis se muestran siempre, más cuando el widget es lo bastante alto; una línea es una cadena o { text, color?, marker?, bold?, wrap?, route? }), route (predeterminado /), textSize ('auto' agranda el texto con el widget hasta donde todas las líneas aún quepan, ajustando las líneas largas a dos filas; 'small' | 'normal' | 'large' | 'xl' o un número 0,6-3), align ('start' | 'center' | 'end'), wrap (las líneas largas ocupan dos filas), y bg / ink / accent. Los colores usan por defecto los tokens CSS --bg, --ink, --accent; deben ser #RRGGBB, #AARRGGBB (el alfa PRIMERO: #9914264A es 60 % opaco), #RGB, rgb()/rgba() o un nombre de color: un valor hsl()/oklch() vuelve a los colores predeterminados. set() devuelve false para un textSize/align desconocido o un color de línea no válido.
  • Un toque abre la app con appmint-shortcut://<package><route>; léelo con launchUrl() / onDeepLink().
  • requestPin() requiere Android 8+ y un launcher compatible. clear() muestra la tarjeta vacĂa "AĂşn no hay nada que mostrar".
  • Solo en la app de Android: can('widget') es 'none' en la web, donde set() y requestPin() devuelven false.

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.