Créer un APK gratuit

API du pont JavaScript

appmintNative (applications créées par IA)

Les wrappers typés que tout projet créé par IA importe depuis @/lib/appmintNative. Chacun choisit le chemin natif dans l'application et un chemin web, ou un honnête none, dans un navigateur - un seul appel fonctionne partout.

appmintNative.actionSheet appmintNative#

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

Exemple

Affiche une feuille du bas « choisissez-en un » - le contrĂŽle natif pour le menu d'actions d'une ligne.

Renvoie : Promise<number | null> - l'indice de l'option choisie, ou null quand l'utilisateur a fermĂ© la feuille (appui sur le fond, Échap, annulation du systĂšme). La fermeture est normale, pas une erreur. NĂ©cessite : rien.

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

L'ouvrir depuis un bouton visible, et depuis un appui long sur la ligne :

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 forme à la Capacitor ActionSheet.showActions({ title?, options }) prend les mêmes options et se résout en { index }, avec -1 pour une fermeture :

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

Remarques :

  • Dans l'application Android, le plugin Capacitor ActionSheet affiche la feuille système ; destructive utilise son style destructif.
  • Ailleurs, c'est une vraie feuille dans la palette de l'application (--surface, --ink, --danger), avec fond, Échap, focus placé dans la feuille et un retour haptique de sélection.
  • Une liste options vide rĂ©sout null aussitĂŽt. Pour tout contenu autre qu'une liste d'actions, utilisez sheet().

appmintNative.ActionSheet appmintNative#

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

Exemple

Documenté avec appmintNative.actionSheet : l'exemple qui s'y trouve illustre aussi celui-ci.

Affiche une feuille du bas « choisissez-en un » - le contrĂŽle natif pour le menu d'actions d'une ligne.

Renvoie : Promise<number | null> - l'indice de l'option choisie, ou null quand l'utilisateur a fermĂ© la feuille (appui sur le fond, Échap, annulation du systĂšme). La fermeture est normale, pas une erreur. NĂ©cessite : rien.

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

L'ouvrir depuis un bouton visible, et depuis un appui long sur la ligne :

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 forme à la Capacitor ActionSheet.showActions({ title?, options }) prend les mêmes options et se résout en { index }, avec -1 pour une fermeture :

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

Remarques :

  • Dans l'application Android, le plugin Capacitor ActionSheet affiche la feuille système ; destructive utilise son style destructif.
  • Ailleurs, c'est une vraie feuille dans la palette de l'application (--surface, --ink, --danger), avec fond, Échap, focus placé dans la feuille et un retour haptique de sélection.
  • Une liste options vide rĂ©sout null aussitĂŽt. Pour tout contenu autre qu'une liste d'actions, utilisez sheet().

appmintNative.addContact appmintNative#

addContact(contact: Contact): Promise<boolean>

Exemple

Enregistre un nouveau contact dans le carnet d'adresses du téléphone. Application Android uniquement : le pont de la coque l'enregistre directement (aucun écran Contacts ne s'ouvre).

Retourne : Promise<boolean> - true une fois enregistré, false quand l'utilisateur a refusé l'autorisation des contacts ou que l'enregistrement a échoué. Rejette avec une Error dont code vaut 'not_enabled' (la compilation n'a pas activé l'écriture des contacts) ou 'foreign_origin', et après 5 minutes d'attente sans réponse. Nécessite : l'option Modifier les contacts à l'étape 4 (Accès) - la compilation l'active quand votre code appelle ceci - et l'autorisation de l'utilisateur à la première utilisation.

Enregistrez un client à partir d'un formulaire.

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

Ignorer un contact déjà enregistré. searchContacts lit le carnet d'adresses : vérifiez donc avant d'ajouter :

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

Distinguez une erreur de compilation d'un refus. Un rejet avec code 'foreign_origin' ou 'not_enabled' est un problĂšme de compilation, false est le choix de l'utilisateur :

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

Remarques : Il enregistre immédiatement - aucun éditeur de contact ne s'ouvre - confirmez donc d'abord avec l'utilisateur dans votre propre écran. Il enregistre les numéros de téléphone comme numéros de mobile et le contact sur l'appareil (sans compte). L'écriture des contacts est sensible : déclarez-la dans votre formulaire de sécurité des données Play et dans votre politique de confidentialité.

appmintNative.alarmSupport appmintNative#

alarmSupport() — @/lib/appmintNative

Exemple

Indique si scheduleAlarm peut programmer une alarme maintenant, et ce qui manque sinon.

Retourne : 'ready', 'not_enabled' (compilé sans Réveil), 'needs_exact_alarm' (Android 12+ : les alarmes exactes sont désactivées), 'needs_notification_permission' (Android 13+), ou 'unavailable' (le web et iOS - n'y affichez pas de commandes d'alarme). Nécessite : 🔔 Réveil à l'étape Accès (étape 4) lors de la compilation.

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

Programmer un réveil quotidien seulement quand le téléphone est prêt, et dire pourquoi quand il ne l'est pas :

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

Masquer l'écran d'alarme sur le web et réagir à la fin d'une alarme, en combinant avec 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)

Exemple

Le nom de l'application, son identifiant de package et sa version, un moyen de fermer l'application, et ses événements - sous la forme du plugin App de Capacitor.

Retourne : getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → la forme de Capacitor (une Promise du handle qui a aussi remove()). Événements : appStateChange ({ isActive }), pause, resume (la page passant en arrière-plan / en revenant), appUrlOpen ({ url }, un lien profond - comme onDeepLink), backButton (l'appui ou le geste retour ; votre gestionnaire décide - l'application ne se ferme pas en plus). Nécessite : rien.

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

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

Fermez l'application depuis un élément « Quitter » qui n'est pas la seule issue :

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

Afficher la version de l'application dans un écran 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>;
}

Remarques : Dans l'application, getInfo lit le plugin Capacitor App ; quand il manque, elle utilise le nom et la version du pont de la coque (id et build valent alors ''), et sur le web document.title avec une version vide. exitApp utilise le closeWindow de la coque dans l'application et ne fait rien sur le web : ne faites donc jamais de Quitter la seule issue. Pour les événements pause/reprise et les liens profonds, voir Capacitor.Plugins.App, launchUrl() et onDeepLink().

appmintNative.appLauncher appmintNative#

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

Exemple

Ouvre une autre application par lien - whatsapp://send?text=…, tel:, mailto:, geo:, ou un lien https que l'OS confie Ă son application - et demande d'abord si cela peut fonctionner.

Retourne : canOpen() → Promise<boolean>. open() → Promise<boolean> (false quand aucune application n'a pris le lien). Nécessite : rien.

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

Appeler ou envoyer un e-mail (aucune vérification nécessaire ; un téléphone a toujours une application pour cela) :

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

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

Ouvrez l'application de cartes avec un lien geo:, et signalez-le quand rien ne l'a pris en charge :

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

Remarques : Dans l'application, il utilise le plugin Capacitor AppLauncher. Sur Android 11+, canOpen ne voit que les schémas que l'application déclare (https, geo, google.navigation, tel, mailto, sms, whatsapp, intent, upi) ; pour les autres, il répond false même si l'application est installée - appelez plutôt open() et vérifiez son résultat. Sur le web, un schéma personnalisé ne peut pas être vérifié, donc canOpen vaut true uniquement pour http(s), et open navigue vers le lien.

appmintNative.appLock appmintNative#

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

Exemple

Reverrouille l'application quand l'utilisateur revient aprÚs qu'elle est restée un moment en arriÚre-plan : elle demande l'empreinte / le visage / le PIN (unlock()), et active l'écran de confidentialité pour que le sélecteur d'applications n'affiche pas votre contenu.

Renvoie : une fonction qui arrĂȘte le verrouillage (appelez-la Ă la dĂ©connexion ou au dĂ©montage). onLocked(true) se dĂ©clenche quand l'application se verrouille, onLocked(false) aprĂšs un dĂ©verrouillage rĂ©ussi. NĂ©cessite : la mĂȘme chose que unlock() - Android : Fingerprint cochĂ© lors de la compilation.

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

Dans le composant racine : const locked = useAppLock(); if (locked) return <LockScreen />;.

ArrĂȘtez le verrouillage Ă la dĂ©connexion, pour qu'un utilisateur dĂ©connectĂ© ne soit pas invitĂ© Ă dĂ©verrouiller :

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

Ne proposez le verrouillage que là où l'appareil peut l'ouvrir, et combinez-le avec unlock() pour un bouton manuel « Verrouiller maintenant » :

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

Remarques : si l'appareil n'a rien pour déverrouiller (can('unlock') === 'none'), elle ne fait rien et renvoie une fonction sans effet - un verrouillage d'application qui ne pourrait jamais s'ouvrir piégerait l'utilisateur. Si l'utilisateur annule, elle redemande jusqu'à cinq fois, puis reste verrouillée (gardez votre écran de verrouillage affiché) jusqu'à ce que l'application passe en arriÚre-plan et revienne. C'est distinct de l'option PIN Lock de l'assistant de compilation, que l'application affiche nativement au lancement.

appmintNative.appUpdate appmintNative#

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

Exemple

Vérifie sur Google Play s'il existe une version plus récente et lance la mise à jour intégrée de Play (télécharge et installe sur place).

Retourne : check() → Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null sur le web, sans services Play, ou quand Play échoue). updateAvailable est la réponse propre du store. start() → Promise<boolean> : true uniquement quand la mise à jour de Play s'est terminée, false quand l'utilisateur a annulé, qu'elle a échoué ou qu'aucune n'était disponible. Nécessite : rien ; l'application doit être installée depuis 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);
  });
}

Comparer le code de version de Play avec celui installé, et traiter null comme « impossible de vérifier ici » :

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

Une bannière React qui vérifie une fois à l'ouverture de l'écran :

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

Remarques : updateAvailable est la réponse propre de Play ; inPlace ajoute que Play peut exécuter sa mise à jour intégrée maintenant. availableVersion est le CODE de version de Play (Play ne donne pas de nom) - comparez-le avec currentVersionCode, jamais avec currentVersion, qui est le nom de version.

appmintNative.askForReview appmintNative#

askForReview(options): Promise<ReviewOutcome>

Exemple

Demande Ă Google Play d'afficher sa propre fenĂȘtre de notation dans l'application et indique ce qui s'est passĂ©.

Retourne : Promise<ReviewOutcome> - { result, openedStore, message? } avec result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' sur le web). { openStore: true } ouvre la fiche Play quand la feuille ne s'est pas affichée. Nécessite : rien ; la feuille n'apparaßt que pour une application installée depuis 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!');
}

Dire à l'utilisateur ce qui s'est passé pour chaque résultat :

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

Demandez une seule fois, aprÚs que l'utilisateur a terminé sa troisiÚme tùche, et retenez que vous avez demandé :

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

Remarques : Demandez après un moment de réussite et rarement. Une installation de test hors Play répond toujours not_from_play.

appmintNative.backgroundLocation appmintNative#

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

Exemple

Une position qui continue d'arriver alors que l'application est en arrière-plan : start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.

Retourne : start(options) → Promise<BgResult> ({ ok, background, backgroundPermission, precise } ou { ok: false, reason }). stop / clearBuffered → Promise<boolean>. buffered() → Promise<LocationFix[]>. requestAlways() → Promise<{ foreground, background, precise, error? }>. onFix(cb) → un désabonnement ; cb reçoit chaque détail appmint:location. Nécessite : Position en arrière-plan dans la compilation ; l'autorisation de localisation (demandez-la d'abord avec 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(); };
}

Demander « Toujours autoriser » avant de démarrer, et expliquer un refus :

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

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

Un hook React qui affiche des points en direct et arrête l'écoute quand l'écran se ferme :

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'

Exemple

RequĂȘtes HTTP natives qui s'exĂ©cutent pendant que l'application est fermĂ©e : enqueue (un envoi unique, enregistrement automatique durable envoyĂ© une fois en ligne), schedule (une requĂȘte rĂ©pĂ©tĂ©e, au moins toutes les 15 minutes), cancel et list.

Retourne : enqueue / schedule → Promise<boolean> ; cancel → Promise<void> ; list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } - pendingDelivery sur Android : une tâche unique pas encore livrée). Nécessite : rien dans l'application Android (elle utilise WorkManager). Sur le web, can('backgroundSync') vaut 'none' et enqueue / schedule se résolvent false.

Enregistrement automatique durable : le même identifiant remplace l'enregistrement qui n'a pas encore été envoyé.

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

Synchronisation répétée, lister et annuler :

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

Événement brut (ce que le wrapper attend sur 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);
});

Remarques : true signifie que la tâche a été mise en file d'attente, pas livrée. false signifie qu'elle n'a pas été mise en file : l'enveloppe a répondu status: 'failed' (une URL ne commençant pas par http, un identifiant vide, ou le système a refusé) ou n'a pas répondu dans les 10 secondes. Le JavaScript de la page ne s'exécute jamais en arrière-plan ; seule cette requête le fait. Une réponse 4xx n'est pas relancée ; les erreurs réseau et 5xx sont relancées jusqu'à 8 fois.

appmintNative.backgroundTasks appmintNative#

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

Exemple

Le code de la page elle-même, exécuté périodiquement application fermée (enrobage typé de AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.

Retourne : register / runNow → Promise<BgResult> ; unregister / done → Promise<boolean> ; list() → les tâches enregistrées avec lastRunAt / lastResult ; isBackgroundRun() / currentRun() sont synchrones. Nécessite : Exécuter le code de la page à l'arrêt dans la compilation.

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

Enregistrez à la connexion, retirez à la déconnexion, et listez ce qui est enregistré :

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

Exécutez une tâche immédiatement depuis un bouton « Synchroniser maintenant » et vérifiez la réponse :

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'

Exemple

Définit ou efface le compteur de non-lus sur l'icône de lancement de l'application.

Retourne : badge.set(count) → Promise<boolean> (false quand le plugin Badge est absent, p. ex. sur le web) ; badge.clear() → Promise<void>. Nécessite : rien sur Android (il utilise le plugin Badge intégré).

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

Répercutez le nombre de non lus quand un message push arrive pendant que l'application est ouverte :

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

Marquez tout comme lu et remettez le compteur Ă zĂ©ro ensemble ; ignorez le badge lĂ oĂč il n'existe pas :

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

Remarques : sur Android, l'aspect dépend du lanceur : beaucoup affichent un nombre, certains seulement un point, d'autres rien - ne mettez pas d'information uniquement dans le badge. Sur le web, can('badge') vaut 'none'.

appmintNative.barcodeFormats appmintNative#

barcodeFormats(): Promise<string[]>

Exemple

Liste les formats de codes-barres et de QR que le BarcodeDetector de cet appareil peut lire.

Renvoie : Promise<string[]> - des noms de formats comme 'qr_code', 'ean_13', 'code_128' ; un tableau vide quand la lecture de codes-barres n'est pas disponible ici. Elle ne rejette jamais. Nécessite : rien.

Affichez l'option « Scanner le code-barres d'un produit » seulement quand l'appareil lit ce format :

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

Restreignez un scan Ă ce que l'appareil prend en charge :

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

Créez un sélecteur de format qui liste uniquement ce que cet appareil peut lire :

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

Remarques :

  • Elle interroge BarcodeDetector.getSupportedFormats() du moteur du navigateur. La WebView de l'application Android le possÚde (mesuré : aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e) ; de nombreux navigateurs de bureau et Safari non, et répondent [].
  • Cette liste concerne readBarcodeFrom() / scanBarcode(). scanCode() dans l'application utilise à la place le scanner de Google et n'en dépend pas.

appmintNative.batteryOptimization appmintNative#

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

Exemple

L'exemption d'optimisation de la batterie : isIgnoring() et request().

Retourne : isIgnoring() → booléen. request() → Promise<string> : 'already_ignoring', 'dialog_opened' (option de compilation « Demander directement d'ignorer l'optimisation de la batterie »), 'settings_opened', 'unavailable', ou 'unsupported' hors de l'application Android. Nécessite : rien pour la liste des paramètres.

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

Affichez l'état actuel sur une ligne de paramÚtres :

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

Demandez l'exemption avant de lancer une longue tĂąche en arriĂšre-plan, avec 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'

Exemple

Capteurs de fitness Bluetooth standard - fréquence cardiaque, vitesse/cadence de cyclisme, puissance, vitesse/cadence de course et batterie du capteur - avec les nombres déjà analysés, dans l'application Android.

Retourne : chaque méthode retourne Promise<void> (setWheelCircumference retourne void). Tout le reste arrive sous forme d'événements de fenêtre appmint:ble avec detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. Nécessite : Bluetooth, que la compilation active d'elle-même quand le code utilise bleSensors. Dans un navigateur (aperçu web), il n'y a pas de pont de capteurs : startScan() répond avec {kind:'error', error:'not_enabled'}, gérez donc cette erreur. Pour le savoir à l'avance, testez can('bluetooth') === 'native' - pas !== 'none' : dans Chrome, can('bluetooth') vaut 'web' (Web Bluetooth), que bleSensors n'utilise pas.

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

Conditionnez l'écran à l'application installée.

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

Arrêtez le scan au premier capteur et surveillez la vitesse de pédalage.

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

Remarques : les analyses durent 15 secondes (stopScan() en termine une plus tĂŽt) ; un seul capteur est connectĂ© Ă la fois. Codes d'erreur : not_enabled, permission_denied, bluetooth_unavailable, bluetooth_off, scan_failed, invalid_address. La vitesse nĂ©cessite deux paquets et la taille de la roue (2096 mm par dĂ©faut). Les mĂȘmes messages vont aussi Ă window.onAppMintBle ; Ă©couter l'Ă©vĂ©nement est plus simple. Pour tout autre appareil Bluetooth, utilisez navigator.bluetooth.

appmintNative.brightness appmintNative#

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

Exemple

Monte la luminosité de l'écran pour une carte d'embarquement ou un code QR, puis rend la main au système. Natif uniquement - une page web ne peut pas piloter le rétroéclairage.

Retourne : get() → Promise<number | null> (0-1 tant que l'application remplace le niveau ; -1 quand l'application suit le niveau système ; null sur le web ou quand il ne peut pas être lu). set(value) → Promise<boolean> (valeur limitée à 0-1 ; false sur le web). restore() → Promise<void>. Nécessite : rien.

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

Mémoriser le niveau, éclaircir, puis le rétablir.

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

Gardez l'écran lumineux et allumé tant qu'un code est affiché.

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

Remarques : cela ne change que la fenĂȘtre de cette application, pas le rĂ©glage de luminositĂ© du tĂ©lĂ©phone, et la valeur forcĂ©e prend fin Ă la fermeture de l'application. restore() dĂ©finit -1 (suivre le systĂšme). get() lit la valeur forcĂ©e de la fenĂȘtre, donc elle vaut -1 tant que vous n'avez pas appelĂ© set() - ce n'est pas le niveau du systĂšme.

appmintNative.browser appmintNative#

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

Exemple

Ouvre une page dans un onglet de navigateur intégré à l'application (Chrome Custom Tabs) - pour les conditions, les pages d'aide ou une page de connexion - et dans un nouvel onglet sur le web.

Renvoie : open() → Promise<boolean> (false quand l'onglet n'a pas pu s'ouvrir). close() → Promise<void>. onClosed(cb) → une fonction de désabonnement ; cb s'exécute quand l'utilisateur ferme l'onglet (dans l'application uniquement). Nécessite : rien.

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 lien vers les conditions dans un composant 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>
  );
}

Fermer l'onglet depuis le code et actualiser quand l'utilisateur revient.

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

Remarques : toolbarColor prend par défaut le jeton --bg de l'application. L'onglet est un navigateur distinct - il ne partage pas les cookies ni le localStorage de l'application. close() ferme l'onglet depuis le code dans l'application ; sur le web, il ne fait rien. onClosed ne se déclenche jamais sur le web.

appmintNative.calendar appmintNative#

calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar

Exemple

Ajoute, liste et supprime des événements dans le calendrier PROPRE au téléphone de l'utilisateur (le fournisseur de calendrier d'Android), et ouvre l'application calendrier à une date.

Renvoie : requestPermission() → Promise<'granted' | 'denied'>. createEvent(input) → Promise<string | null> (l'identifiant de l'événement ; null quand l'utilisateur a refusé l'accÚs au calendrier - elle rejette quand l'événement lui-mĂȘme n'a pas pu ĂȘtre créé, par ex. dates invalides). listEvents({ from, to }) → Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (heures en ms epoch). deleteEvent(id) / openCalendar(date?) → Promise<boolean>. Seul createEvent rejette, et seulement pour un événement qui n'a pas pu ĂȘtre créé. Nécessite : activez Calendar à l'étape 4 (Access) lors de la compilation - la compilation le coche pour vous quand elle voit calendar.createEvent(, calendar.listEvents( ou calendar.requestPermission( dans le code.

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

Lister cette semaine, supprimer, ouvrir :

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

Ajoutez au calendrier seulement quand l'écriture est possible.

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

Remarques : sur le web, createEvent télécharge un fichier .ics que l'utilisateur ouvre dans son calendrier (renvoie son UID), listEvents renvoie [] (can('calendarRead') vaut 'none'), et deleteEvent / openCalendar renvoient false. Les rappels et les événements récurrents sont disponibles sur Capacitor.Plugins.Calendar, pas via cette enveloppe.

appmintNative.camera appmintNative#

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

Exemple

Prend une photo avec la caméra ou choisit des photos dans la galerie : le plugin Capacitor Camera dans l'application, un champ de fichier avec capture sur le web.

Retourne : take() → Promise<Photo | null> avec { dataUrl, mime } ; null en cas d'annulation ou d'autorisation refusée. pick() → Promise<Photo[]>, vide en cas d'annulation. Nécessite : Caméra à l'étape 4 (Accès) - la compilation l'active quand votre code appelle camera.take.

Prendre une photo de profil avec la caméra avant.

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

Choisissez une photo dans la galerie avec take, ou plusieurs avec 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);

Importez une photo en transformant l'URL de données en 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 });
}

Remarques : Options : source ('camera' par défaut, ou 'photos'), quality 0-100 (80 par défaut), direction ('rear' par défaut, ou 'front'), allowEditing. Les photos ne sont pas enregistrées dans la galerie. Une autorisation refusée résout null / [], sans jamais lever d'erreur - affichez un court message pour que l'utilisateur comprenne pourquoi rien ne s'est passé. La caméra est sensible : déclarez-la dans votre formulaire Sécurité des données de Play.

appmintNative.can appmintNative#

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

Exemple

Vous indique où une fonctionnalité s'exécutera réellement sur la plateforme actuelle, pour que l'application n'affiche que des commandes qui fonctionnent.

Retourne : une chaîne, de façon synchrone : 'native' (l'implémentation propre à l'application - le pont de la coque ou un plugin Capacitor intégré à l'application), 'web' (une vraie norme web dans ce navigateur) ou 'none' (indisponible ici - n'affichez pas le contrôle). Nécessite : rien.

Masquez une commande qui ne peut pas fonctionner sur cette plateforme :

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

Changer la formulation selon la surface :

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

Noms des capacités : 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.

Affichez une indication plutôt qu'une commande inerte, et transformez une fonctionnalité en simple oui/non avec 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';

Remarques :

  • Un plugin Capacitor intégré à l'application répond toujours 'native' ; sinon le pont de la coque est vérifié, puis la norme web.
  • toast, actionSheet, dialog, statusBar et swipe ont une vraie version web : ils ne valent donc pas 'none' dans un navigateur.
  • contactList, contactAdd, smsSend, smsList, callLog, shortcuts, shareTarget et widget valent 'none' sur le web.
  • Certaines fonctionnalitĂ©s n'existent que si la compilation les a activĂ©es (accĂšs aux dossiers, clĂ©s sĂ©curisĂ©es, contrĂŽles multimĂ©dias, SMS, contacts). Demandez Ă can(), pas isNativeApp().
  • Un nom inconnu retourne 'none'. Pour un simple oui/non, utilisez has(cap).

appmintNative.clearPushInbox appmintNative#

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

Exemple

Vide la boßte de réception AppMint Push sur ce téléphone. Définitif - il n'y a pas d'annulation.

Retourne : boolean de façon synchrone - true une fois vidée ; toujours false sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

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

N'affichez Effacer que lorsqu'il y a quelque chose à effacer, et indiquez le résultat :

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

Marquez tout comme lu plutĂŽt que de le supprimer, ce qui ne fait rien perdre :

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

Remarques : les notifications de la barre d'état et votre historique d'envoi dans AppMint ne sont pas touchés ; les nouveaux messages arrivent toujours.

appmintNative.clearShared appmintNative#

clearShared(): void

Exemple

Oublie le texte ou le lien qu'une autre application a partagé vers celle-ci, une fois que votre application l'a enregistré.

Retourne : rien. Nécessite : rien (ne fait rien sur le 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;
}

Ne vider le partage qu'après son enregistrement, et le garder quand l'enregistrement échoue :

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

Gérez un partage qui arrive pendant que l'application est ouverte :

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

Remarques : sharedText() conserve un partage jusqu'Ă ce que ceci soit appelĂ©, volontairement, pour qu'un Ă©cran montĂ© tardivement le voie encore. N'appelez clearShared() qu'APRÈS l'enregistrement sĂ»r du partage - si l'enregistrement Ă©choue, laissez-le pour que l'utilisateur ne le perde pas.

appmintNative.click appmintNative#

click() — @/lib/appmintNative

Exemple

Joue une fois le son des touches de l'application, pour vos propres boutons.

Retourne : void. Nécessite : Son des touches activé à l'étape 3 (Intégrer) lors de la compilation. Sur le web, et dans les applications compilées sans Son des touches, il ne fait rien.

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

Laissez l'utilisateur couper le son dans vos propres paramĂštres :

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
}

La lancer depuis une ligne de liste avec 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>;
}

Remarques : Avec Suivre le réglage du téléphone (recommandé), le clic est silencieux quand l'utilisateur a désactivé les « Sons tactiles » sur le téléphone. Avec Toujours jouer à chaque appui, chaque appui fait déjà un clic, les appels click() supplémentaires ne sont donc pas nécessaires. Android uniquement (le WebToApk.playClick).

appmintNative.Clipboard appmintNative#

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

Exemple

Le presse-papiers sous la forme du plugin Clipboard de Capacitor - le mĂȘme code que copy() / paste().

Retourne : write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value vaut '' quand le presse-papiers est vide). Nécessite : rien.

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

Copier un code et confirmer avec un toast :

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

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

Écrivez un lien quand il n'y a pas de texte, et traitez un presse-papiers vide comme rien à coller :

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

Remarques : Dans l'application, elle utilise le plugin Clipboard de Capacitor ; dans un navigateur, navigator.clipboard. write n'envoie que string (ou url quand string est vide) - pas d'images. Dans un navigateur, read() peut lever une erreur si l'utilisateur refuse l'accÚs ; dans l'application, un presse-papiers vide donne '', pas une erreur. Interrogez can('clipboardRead') avant d'afficher un contrÎle Coller.

appmintNative.collapsingHeader appmintNative#

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

Exemple

Fait rĂ©agir un en-tĂȘte Ă grand titre au dĂ©filement comme le font les en-tĂȘtes natifs, en publiant une valeur de 0 Ă 1 que votre CSS peut utiliser.

Retourne : une fonction de désabonnement qui supprime aussi la variable et l'attribut. Nécessite : rien.

Il dĂ©finit --collapse (de 0 en haut Ă 1 aprĂšs range px de dĂ©filement) sur l'en-tĂȘte, et ajoute data-collapsed une fois complĂštement rĂ©duit :

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

Arrêter l'écoute depuis un script simple ; cela supprime aussi --collapse et 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);

Remarques :

  • scroller par dĂ©faut est la fenĂȘtre (une page qui dĂ©file en entier) ; range par dĂ©faut est 64 px.
  • L'Ă©couteur de dĂ©filement est passif et se met Ă jour une fois par image d'animation, il ne ralentit donc pas le dĂ©filement.

appmintNative.composeSms appmintNative#

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

Exemple

Ouvre l'application de messagerie avec le numéro et le texte préremplis ; l'utilisateur appuie sur envoyer. Fonctionne dans l'application Android (pont de la coque) et sur le web (un lien sms:).

Retourne : boolean immédiatement - true quand l'application de messagerie a été invitée à s'ouvrir. Nécessite : aucune autorisation.

Un bouton « Écrire à la boutique ».

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 réutilisable qui masque le bouton quand il n'y a pas de nombre.

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

Invitez un ami ; l'utilisateur appuie sur envoyer.

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

Remarques : Préférez ceci à sendSms() pour tout ce que l'utilisateur écrit ou approuve - rien ne quitte le téléphone sans que l'utilisateur appuie sur envoyer. Sur le web, il navigue vers sms:…?body=…. Un appel composeSms( n'active pas SMS (lire et envoyer) lors de la compilation, il n'ajoute donc aucune autorisation restreinte par Play.

appmintNative.copy appmintNative#

copy(text: string): Promise<boolean>

Exemple

Place du texte dans le presse-papiers du système.

Retourne : Promise<boolean> - true quand le texte a été copié, false quand le presse-papiers n'est pas disponible ou que le navigateur a refusé. Nécessite : rien.

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 forme à la Capacitor fait la même chose :

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

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

Signalez les deux issues, avec un retour haptique seulement en cas de succĂšs :

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

Remarques : dans l'application, le plugin Capacitor Clipboard écrit le texte (une erreur du plugin rejette la promesse au lieu de renvoyer false). Dans un navigateur, il utilise navigator.clipboard.writeText, qui nécessite une page https et généralement un appui de l'utilisateur.

appmintNative.copyImage appmintNative#

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

Exemple

Copie une image dans le presse-papiers pour la coller dans les applications de messagerie et de courrier : le presse-papiers de l'application dans l'application Android, l'API Async Clipboard sur le web.

Retourne : Promise<boolean> - true une fois copié. Nécessite : rien ; conditionnez le contrôle à 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');
}

Copiez une photo depuis l'appareil photo, puis confirmez-le avec 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');
}

N'affichez le bouton que là où le presse-papiers accepte les images, et copiez une image depuis une 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>
  );
}

Remarques : le PNG est le type le plus sûr partout ; l'application Android accepte aussi le JPEG et le WebP.

appmintNative.Device appmintNative#

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

Exemple

La forme @capacitor/device : modèle du téléphone, version du système, un identifiant stable et la langue. Elle utilise le plugin Capacitor Device dans l'application et la réponse du navigateur sur le web.

Retourne : getInfo() → Promise<DeviceInfo & { operatingSystem, isVirtual }> (avec Capacitor aussi manufacturer, webViewVersion, …) ; getId() → Promise<{ identifier }> ; getLanguageCode() → Promise<{ value }> (deux lettres, p. ex. "en"). Nécessite : rien.

Affichez le modÚle et la version de l'OS sur un écran à propos.

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 identifiant stable et le code de langue.

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

Affichez la version de l'application et le modèle de l'appareil dans un pied de page de paramètres. Le rapport de synchronisation donne la version de l'application ; le plugin ajoute le système d'exploitation.

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

Remarques : Dans l'application, deviceInfo() de la synchronisation remplit aussi model et osVersion ; Device.getInfo() ajoute operatingSystem, isVirtual et les champs supplémentaires du plugin. Sur le web, operatingSystem vaut 'unknown' et model est vide. getId() sur le web est l'identifiant localStorage issu de installId() et lève une exception quand le stockage est bloqué.

appmintNative.deviceInfo appmintNative#

deviceInfo(): DeviceInfo

Exemple

Vous indique de façon synchrone où l'application s'exécute (platform) et la langue, plus le rapport complet de l'appareil de l'enveloppe Android dans l'application Android.

Retourne : DeviceInfo immédiatement : toujours { platform, language } - platform vaut 'android' dans l'application et 'web' dans un navigateur. Dans l'application Android, il remplit aussi model, manufacturer, osVersion (p. ex. "14") et appVersion (p. ex. "1.4.0"), et porte chaque clé de WebToApk.getDeviceInfo() : android, hardware, screen, app, runtime, installId. Sur le web, il ajoute osVersion (le user agent). Nécessite : rien.

Brancher selon la plateforme pendant le rendu.

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

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

Lire les détails Android. Les champs courants sont en haut ; le reste du rapport de la coque reste dans ses groupes imbriqués.

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

Choisissez la bonne commande selon la plateforme.

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

Remarques : model et appVersion ne sont renseignés que dans l'application. Sur le web, osVersion est la chaßne user-agent du navigateur, pas une version d'OS. Une erreur est levée si la réponse du shell n'est pas du JSON valide (un vrai défaut, jamais attendu).

appmintNative.Dialog appmintNative#

Dialog.alert / Dialog.confirm / Dialog.prompt

Exemple

De vraies boßtes de dialogue systÚme : un message, une question oui/non, une question texte sur une ligne. L'application utilise le plugin Capacitor Dialog ; le web utilise le propre alert / confirm / prompt.

Renvoie : alert({ title?, message }) → Promise<void>. confirm({ title?, message, okButtonTitle?, cancelButtonTitle? }) → Promise<{ value: boolean }>. prompt({ title?, message, inputPlaceholder?, inputText? }) → Promise<{ value: string; cancelled: boolean }>. confirm et prompt ne rejettent jamais (la fermeture est une réponse) ; alert rejette seulement quand il ne peut pas ĂȘtre affiché (pas de message). Nécessite : rien.

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

Alerte et saisie :

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

Confirmez avant de vous déconnecter. Fermer la boîte de dialogue compte comme « non ».

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

Remarques : Fermer une confirmation donne { value: false } ; fermer une saisie donne { value: '', cancelled: true }. Les boîtes de dialogue bloquent l'écran - utilisez-les pour de vraies décisions, et toast() pour les messages du genre « Enregistré ». prompt ne transmet pas de titres de boutons personnalisés au plugin.

appmintNative.dismissPushMessage appmintNative#

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

Exemple

Masque un message de la boßte de réception AppMint Push (balayage pour retirer). Annulez-le avec restorePushMessage(id).

Retourne : boolean de façon synchrone - true s'il était masqué. Toujours false sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

Affichez la boßte de réception avec un gestionnaire de balayage et un état d'annulation, pour qu'un balayage ne soit jamais définitif.

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

Masquez d'un coup tous les messages lus, et indiquez combien ont été masqués.

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

Remarques : seule la boßte de réception de ce téléphone change ; la notification dans la barre et votre historique d'envoi restent.

appmintNative.docScanner appmintNative#

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

Exemple

Ouvre le scanner de documents du systÚme - détection des bords, redressement, plusieurs pages - le scanner ML Kit de Google dans l'application Android. Les pages reviennent sous forme d'URL de données JPEG.

Renvoie : Promise<{ pages: string[]; pdfPath?: string } | null> - null quand l'utilisateur annule, que le scanner ne peut pas dĂ©marrer, ou sur le web. isSupported() → Promise<boolean> : si ce tĂ©lĂ©phone peut scanner (services Google Play) ; false sur le web. NĂ©cessite : rien Ă activer (sur Android, le scanner s'exĂ©cute dans les services Google Play et ne nĂ©cessite aucune autorisation de camĂ©ra).

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

Interrogez d'abord le téléphone, car un appareil ancien peut ne pas avoir le scanner, puis conservez les pages.

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

Numériser, puis envoyer la première page avec l'assistant de transfert de fichiers.

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

Remarques : pageLimit 0 (par défaut) = sans limite ; quality 0,1-1 (0,85 par défaut). Avec pdf: true, pdfPath est un chemin de fichier dans le cache de l'application, pas une URL que la page peut charger. Les URL de données sont volumineuses - redimensionnez-les ou envoyez-les plutôt que d'en garder beaucoup dans localStorage. Pour une simple photo, utilisez camera.take().

appmintNative.enableNotifications appmintNative#

enableNotifications(): Promise<EnableNotificationsResult>

Exemple

Ramène un utilisateur qui a refusé ou ignoré les notifications, depuis un bouton qu'il a touché : la boîte de dialogue d'autorisation tant qu'elle peut encore s'afficher, sinon (application Android) les paramètres de notification de cette application.

Retourne : Promise<{ state, changed, openedSettings }> - state vaut 'granted' | 'prompt' | 'blocked' | 'unavailable', changed est vrai quand il diffère d'avant l'appel, openedSettings est vrai quand l'écran des paramètres a été utilisé. Ne rejette jamais. Sur le web, elle demande via Notification.requestPermission(). Nécessite : les notifications dans la compilation (elles sont activées dès que l'application envoie ou reçoit des notifications).

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

Remarques : n'affichez la banniĂšre que pour 'prompt' et 'blocked' ; masquez-la pour 'granted' et 'unavailable'. Demandez depuis un appui, jamais au lancement.

appmintNative.fileTransfer appmintNative#

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

Exemple

TĂ©lĂ©charge ou envoie un fichier avec une fonction de rappel de progression. Dans l'application, le transfert s'exĂ©cute nativement (Capacitor FileTransfer) ; sur le web, il utilise fetch / XMLHttpRequest avec la mĂȘme fonction de rappel.

Retourne : download(url, fileName, opts?) → Promise<{ path?: string; blob?: Blob }> - path (un fichier dans le cache de l'application) dans l'application, blob sur le web (où le fichier est aussi enregistré via le navigateur). upload(url, file, opts?) → Promise<{ responseCode: number; responseBody: string }>. upload se résout quel que soit le statut HTTP répondu par le serveur (vérifiez responseCode), dans l'application comme sur le web ; les deux rejettent quand le transfert lui-même échoue (pas de connexion, fichier invalide). Nécessite : rien à activer.

Télécharger avec une barre de progression :

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

Envoyer un File depuis 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})`);
}

Téléchargez avec un état de progression, puis ouvrez le fichier, dans un seul 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 };
}

Remarques : fraction vaut 0 tant que le serveur n'envoie pas de longueur. Dans l'application, un téléchargement atterrit dans le dossier CACHE de l'application (pas dans les Téléchargements de l'utilisateur) ; pour donner une copie à l'utilisateur, utilisez saveFile. Pour un envoi, l'application copie d'abord le fichier dans son cache, donc les très gros fichiers demandent autant d'espace libre. method (par défaut 'POST') et fileName sont des options d'envoi facultatives.

appmintNative.fileViewer appmintNative#

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

Exemple

Ouvre un document (PDF, image, fichier bureautique) dans l'application de visualisation du téléphone. Sur le web, il ouvre l'adresse dans un nouvel onglet.

Retourne : Promise<boolean> - true quand la visionneuse s'est ouverte, false quand elle a échoué (aucune application pour ce type, mauvais chemin) ou que la cible ne peut pas être ouverte sur cette plateforme. Nécessite : rien à activer ; il utilise le plugin Capacitor FileViewer fourni dans l'application.

Ouvrir un document depuis le 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.');
}

Ouvrez un fichier téléchargé d'abord avec fileTransfer.download (un chemin local dans l'application) :

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

Ouvrez un lien depuis un bouton et dites à l'utilisateur quand aucune visionneuse n'a pu l'ouvrir.

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

Remarques : une cible http(s) utilise openDocumentFromUrl ; tout le reste est traité comme un chemin local (file:// est retiré). Sur le web, seules les cibles http(s):, blob: et data: s'ouvrent ; un chemin local répond false.

appmintNative.folder appmintNative#

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

Exemple

Un dossier entier que l'utilisateur autorise une fois et que l'application conserve entre les lancements, avec de vraies opérations sur les fichiers à l'intérieur (dans l'application : le pont de dossier de l'enveloppe).

Retourne : chaque membre est asynchrone - connected() → boolean, label() → string, request() → boolean (faux en cas d'annulation), disconnect() → void, list(path) → [{ name, uri, isDirectory, length, lastModified }], readText(path) → string | null (null = fichier inexistant), writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean, stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. Nécessite : une application créée par l'IA active d'elle-même l'interrupteur Accès natif aux dossiers (SAF) quand le code utilise folder ; pour une compilation manuelle, cochez-le à l'étape 4 (Accès).

Se connecter, afficher le dossier et se déconnecter :

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

Lire, écrire, lister, renommer et supprimer - chaque chemin est relatif au dossier autorisé ('' en est la racine) :

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

Sauvegardez une note dans le dossier autorisé, et dites-le clairement quand aucun dossier n'est connecté.

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

Remarques : can('folderAccess') vaut 'none' sur le web - il n'existe pas d'équivalent de dossier persistant dans un navigateur de téléphone, n'affichez donc pas la fonctionnalité là-bas (utilisez pickFile() / saveFile() pour des fichiers uniques). N'appelez request() que depuis un appui de l'utilisateur. .. et les chemins absolus sont refusés. Le dossier autorisé lui-même ne peut jamais être supprimé. stat() fonctionne pour les fichiers et sous-dossiers (un dossier a isDirectory: true et size: 0) ; stat('') - la racine - vaut null, utilisez label() pour elle. request() peut être rappelé après une annulation.

appmintNative.fullscreen appmintNative#

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

Exemple

Masque les barres système (dans l'application) ou passe en plein écran du navigateur (sur le web), pour une vidéo, un jeu ou une vue de lecture.

Retourne : enter() et exit() retournent Promise<void> et ne rejettent jamais ; isOn() retourne boolean de façon synchrone. Nécessite : rien.

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

Quittez le plein écran quand la vue se ferme :

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

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

Masquer les barres systÚme pour une vue de lecture et suivre l'état depuis un bouton.

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

Remarques :

  • Dans l'application, c'est le plein écran propre à l'enveloppe (les barres d'état et de navigation se masquent) ; il est toujours disponible pour la page.
  • Sur le web, l'API Fullscreen ne fonctionne qu'après un appui de l'utilisateur. Une demande refusée est ignorée silencieusement : lisez donc isOn() ensuite plutôt que de supposer.

appmintNative.geofences appmintNative#

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

Exemple

Des géorepérages qui se déclenchent application fermée et survivent aux redémarrages : add, remove, list, events, clearEvents, onEvent, available.

Renvoie : add(spec) → Promise<BgResult> rĂ©solu une fois que les services Play l'ont acceptĂ© ou refusĂ© ({ id, ok, reason? } - background_permission_required, location_off, too_many_geofences, …). remove / clearEvents → Promise<boolean>. list() → Promise<GeofenceSpec[]>. events() → Promise<GeofenceEvent[]> (les 100 derniers, y compris ceux survenus pendant que l'application Ă©tait fermĂ©e). onEvent(cb) → un dĂ©sabonnement. NĂ©cessite : Geofences dans la compilation et « Toujours autoriser » (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)));

Listez les géorepérages actifs et les transitions enregistrées pendant que l'application était fermée.

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

Ajoutez une zone de travail pour l'entrée et la sortie, répondez à un refus selon sa raison, et supprimez-la plus tard.

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

Exemple

La forme @capacitor/geolocation : le plugin Capacitor Geolocation dans l'application, le navigator.geolocation standard sur le web. Tous retournent l'objet position du W3C.

Retourne : getCurrentPosition(opts) → Promise<GeolocationPosition> (rejette en cas de refus, de délai dépassé ou d'absence de position) ; watchPosition(opts, cb) → Promise<string> (l'identifiant de suivi) ; clearWatch({ id }) → Promise<void> ; checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (chaque 'granted' | 'denied' | 'prompt' ; sur Android 12+, un utilisateur qui choisit « Approximative » n'accorde que coarseLocation). Nécessite : GPS / Position à l'étape 4 (Accès) - la compilation active les deux quand votre code appelle getCurrentPosition ou watchPosition.

Où suis-je, une fois.

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

Suivre un parcours, puis arrĂȘter. Le callback reçoit null et une erreur quand une position Ă©choue.

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

Obtenez une position ponctuelle. Retourne null quand l'utilisateur refuse.

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

Remarques : Un simple navigator.geolocation fonctionne aussi dans l'application ; utilisez ceci quand vous voulez la forme Capacitor. Sur Android 12+, l'utilisateur peut n'autoriser qu'une position approximative - vous obtenez quand même une position, simplement moins précise. Sur le web, requestPermissions() fait une vraie demande de position pour déclencher l'invite du navigateur. La position est une donnée personnelle : déclarez-la dans votre formulaire de sécurité des données Play.

appmintNative.getNotificationState appmintNative#

getNotificationState(): Promise<NotificationState>

Exemple

Indique si les notifications sont activées, et sinon si l'application peut encore demander ou si seuls les Paramètres peuvent régler cela.

Renvoie : Promise<'granted' | 'prompt' | 'blocked' | 'unavailable'>. Sur le web, Notification.permission converti de la mĂȘme façon (default → 'prompt', denied → 'blocked') ; 'unavailable' lĂ oĂč il n'y a aucune notification. NĂ©cessite : rien.

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

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

Remarques : vĂ©rifiez de nouveau quand l'application revient au premier plan (visibilitychange) - l'utilisateur a peut-ĂȘtre changĂ© cela dans les ParamĂštres.

appmintNative.getPushTags appmintNative#

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

Exemple

Lit les tags AppMint Push de cette installation sous forme d'objet.

Renvoie : Record<string, string> de façon synchrone - par ex. { plan: 'pro', cart: 'open' } ; {} quand il n'y en a aucun et sur le web. NĂ©cessite : Android : AppMint Push activĂ© Ă l'Ă©tape Integrate (Ă©tape 3) lors de la compilation.

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

Définissez plusieurs tags en une seule modification avec setPushTags quand un écran en change plus d'un, et videz une valeur pour supprimer son tag :

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

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

Relisez les tags pour n'afficher une astuce que lorsque cette installation a un tag :

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

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

Remarques : c'est ce que le téléphone conserve ; le centre d'envoi le voit après la prochaine synchronisation de cette installation.

appmintNative.getPushUser appmintNative#

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

Exemple

Lit l'identifiant d'utilisateur que cette installation a donné en dernier à AppMint Push avec setPushUser.

Retourne : string de façon synchrone - l'identifiant enregistré, ou '' quand déconnecté, jamais défini, ou sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

Appelez setPushUser avec l'identifiant de compte après la connexion et avec '' à la déconnexion ; il retourne false quand l'identifiant est refusé ou qu'AppMint Push est désactivé :

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

Lisez l'identifiant enregistré dans un hook React, avec un tag appartenant à cet utilisateur :

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

Remarques : ceci est ce que détient le téléphone ; le centre d'envoi voit un changement après la prochaine synchronisation de l'installation, quelques secondes plus tard en ligne.

appmintNative.getVolume appmintNative#

getVolume(stream) — @/lib/appmintNative

Exemple

Lit le niveau de volume propre au téléphone pour un flux : 'media', 'alarm', 'ring', 'notification' ou 'call'.

Retourne : { stream, current, max, percent, muted? } (current/max selon les paliers propres au téléphone, percent 0-100), ou null sur le web. Nécessite : rien.

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 répond null sur le web, n'affichez donc le niveau que lorsque c'est un objet :

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

Suivez les touches matérielles avec onVolumeChange, et relevez un niveau trop bas avec 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

Exemple

Une courte vibration naturelle pour les appuis et les résultats. Appelez-la à chaque appui significatif.

Retourne : void (sans attendre). kind : 'light' (par défaut), 'medium', 'heavy', 'selection', 'success', 'warning', 'error'. Nécessite : Vibration à l'étape 4 (Accès) ; les compilations par IA l'activent pour vous quand le code appelle 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>
  );
}

Pour les onglets, les bascules et les sélecteurs, utilisez 'selection' ; pour une confirmation de suppression, utilisez 'heavy':

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

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

Un bouton « j'aime » qui fait un tic léger à l'activation et plus ferme à la désactivation :

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

Remarques : Dans l'application, cela utilise le plugin Capacitor Haptics (impact, notification, selectionChanged). Sur le web, cela utilise navigator.vibrate quand le navigateur l'a, et ne fait rien ailleurs ; ne lève jamais d'erreur. Pour des rythmes plus longs, utilisez hapticPlay.

appmintNative.hapticPlay appmintNative#

hapticPlay(pattern) — @/lib/appmintNative

Exemple

Joue un motif de vibration riche pour un moment spécial (une victoire, une série, un battement de cœur). Utilisez haptic() pour les appuis normaux.

Retourne : Promise<boolean> : true quand le motif a été joué, false quand il n'a pas pu l'être. Nécessite : Vibration à l'étape 4 (Accès).

Motifs nommés : '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();
}

Votre propre motif : une liste 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
]);

Combinez-la avec haptic() : un retour tactile normal pour chaque réponse, le long motif seulement pour le moment spécial :

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

Remarques : L'application utilise le plugin Capacitor AppwrightHaptics (intensité par battement quand le téléphone le permet). Le web n'obtient que le rythme. sharpness est accepté mais sans effet sur Android. haptic.play(pattern) est la même fonction.

appmintNative.Haptics appmintNative#

Haptics — @/lib/appmintNative

Exemple

L'objet Haptics à la façon de Capacitor : mêmes noms et mêmes options que @capacitor/haptics, exécuté avec le même code que haptic().

Renvoie : chaque méthode renvoie void (pas une Promise ; await dessus est sans effet). Nécessite : Vibrate à l'étape 4 (Access) ; les compilations IA l'activent pour vous quand le code utilise 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 sélecteur qui émet un tic à chaque changement de valeur :

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 formulaire qui confirme par une vibration de notification et ajoute une courte vibration quand un champ est incorrect :

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

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

Remarques : selectionStart et selectionChanged produisent tous deux un seul tic de sélection ; selectionEnd ne fait rien. Fonctionne dans l'application (plugin Capacitor Haptics) ; sur le web, il utilise navigator.vibrate si disponible, sinon ne fait rien.

appmintNative.has appmintNative#

has(cap: Capability): boolean

Exemple

La forme oui/non de can() : vrai quand la fonctionnalité fera quelque chose de réel sur cette surface.

Renvoie : boolean, de façon synchrone - exactement can(cap) !== 'none'. NĂ©cessite : rien.

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

Conditionnez toute une fonctionnalité :

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

Choisir un libellé selon l'endroit où cela s'exécute : has() décide s'il faut afficher le contrôle, can() indique de quel type il s'agit :

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

Remarques : elle accepte les mĂȘmes noms de capacitĂ©s que can() (voir cette entrĂ©e pour la liste complĂšte). Utilisez can() quand vous avez besoin de savoir OÙ elle s'exĂ©cute ('native' ou 'web'), par exemple pour changer un libellĂ©.

appmintNative.hideKeyboard appmintNative#

hideKeyboard(): void

Exemple

Ferme le clavier à l'écran - par exemple quand un formulaire est envoyé ou qu'une feuille s'ouvre par-dessus le champ.

Retourne : rien. Nécessite : rien.

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

Fermez le clavier quand une feuille s'ouvre sur le champ, pour que la feuille ne soit pas cachée derriÚre :

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

La fermer quand l'utilisateur touche n'importe où hors d'un champ de texte :

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

Remarques : Dans l'application, elle demande au plugin Capacitor Keyboard de masquer le clavier. Sur le web, elle retire le focus du champ actif, ce qui ferme le clavier dans un navigateur. On peut l'appeler sans risque quand aucun clavier n'est ouvert.

appmintNative.inAppCampaigns appmintNative#

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

Exemple

Les messages intégrés à l'application que cette installation détient en ce moment, sous forme de {id, name}[] : les campagnes du créateur dont cet utilisateur fait partie de l'audience.

Retourne : { id: string; name: string }[] de façon synchrone ; [] sur le web et sans AppMint Push. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

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

Une liste de débogage dans un écran d'administration :

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

Ignorez votre propre indication quand le créateur diffuse déjà un message portant ce nom :

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

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

appmintNative.installId appmintNative#

installId(): string

Exemple

Donne un identifiant aléatoire stable pour cette installation : l'identifiant propre du shell dans l'application Android, un identifiant aléatoire enregistré dans localStorage sur le web.

Renvoie : string immédiatement. Nécessite : rien.

Lier un essai gratuit à l'appareil.

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

Comptez cette installation une seule fois dans vos propres statistiques.

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

Afficher un court code d'assistance que vous pourrez retrouver plus tard, avec un bouton de copie.

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

Remarques : Il reste identique entre les mises à jour de l'application et change à la désinstallation ou à « effacer les données ». Ce n'est pas un identifiant matériel. Il lève une exception sur le web quand localStorage est bloqué, volontairement - un identifiant vide partagé mélangerait les utilisateurs.

appmintNative.isAlarmRinging appmintNative#

isAlarmRinging() — @/lib/appmintNative

Exemple

Indique si l'un des réveils de l'application sonne en ce moment.

Retourne : boolean, de façon synchrone ; toujours false sur le web. Nécessite : 🔔 Réveil à l'étape Accès (étape 4) lors de la compilation.

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

Affichez les boutons ArrĂȘter et Reporter pendant qu'une alarme sonne :

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

VĂ©rifiez d'abord la prise en charge, pour que la page n'affiche les commandes d'alarme que lĂ oĂč une alarme peut sonner :

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

Exemple

Indique si le code s'exécute dans l'application Android installée (le pont de la coque window.WebToApk est présent).

Renvoie : boolean immédiatement. false dans un navigateur. Nécessite : rien.

Affichez un badge « Application Android » et un lien de téléchargement dans le navigateur.

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

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

Étiquetez vos événements d'analyse avec leur provenance.

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

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

Masquez la banniÚre « Obtenez l'application » à l'intérieur de l'application.

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

Remarques : platform() donne la mĂȘme rĂ©ponse sous forme de chaĂźne ('android' dans l'application, 'web' dans un navigateur). Pour dĂ©cider si une fonctionnalitĂ© peut ĂȘtre affichĂ©e, utilisez plutĂŽt can('<capability>') : elle sait quelle plateforme a quelle fonctionnalitĂ©.

appmintNative.keepAlive appmintNative#

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

Exemple

Garde l'application en cours d'exécution en arrière-plan avec une notification de service au premier plan (start, update, stop, isRunning, onState, available).

Retourne : start / update → Promise<BgResult> ({ ok, reason?, type?, notificationShown? } ; reason: 'unsupported' dans un navigateur ou sur iOS, 'not_enabled' quand la compilation n'a pas coché Rester actif en arrière-plan). stop → Promise<boolean>. isRunning() → booléen. onState(cb) → une fonction de désabonnement ; cb reçoit le détail appmint:background-service. Nécessite : Rester actif en arrière-plan dans la compilation.

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

Arrêtez le service, et lisez pourquoi start a été refusé :

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

Gardez un bouton React synchronisé avec le service, et affichez la progression dans la notification :

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>

Exemple

Garde l'écran allumé pendant l'affichage d'une recette, d'un itinéraire, d'un code QR ou d'un minuteur, puis le laisse se mettre en veille.

Retourne : Promise<boolean> - true quand la demande a pris effet, false quand rien n'a pu garder l'écran allumé (ou que le navigateur a refusé). Nécessite : rien.

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

Gardez l'écran allumé uniquement pendant qu'un minuteur tourne, et dites à l'utilisateur quand cela n'a pas pu être fait.

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

Vérifiez d'abord la capacité et reprenez le verrou quand l'onglet revient au premier plan (le web le libère quand la page est masquée).

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

Remarques : dans l'application, elle utilise le plugin KeepAwake (l'indicateur d'Ă©cran allumĂ© de la fenĂȘtre) ; sur le web, la Screen Wake Lock API, que les navigateurs libĂšrent d'eux-mĂȘmes quand l'onglet est masquĂ© - rappelez keepAwake(true) quand la page redevient visible si l'Ă©cran doit rester allumĂ©. Cela ne fonctionne que lorsque l'application est au premier plan.

appmintNative.Keyboard appmintNative#

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

Exemple

Le clavier logiciel sous la forme du plugin Keyboard de Capacitor : fermez-le, et sachez quand l'espace qu'il couvre change.

Retourne : hide() → Promise<void>. addListener(event, handler) → la forme de Capacitor : une Promise du handle qui a aussi remove() lui-même, donc await, .then(h => h.remove()) et .remove() fonctionnent tous. event vaut 'keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide' ou 'keyboardDidHide' ; le gestionnaire reçoit { keyboardHeight }. Nécessite : rien.

Fermez le clavier quand un formulaire est envoyé ou qu'une feuille s'ouvre par-dessus le champ :

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

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

Écouter (supprimez le handle au démontage) :

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

Combinez les deux pour faire défiler le champ actif en vue à l'ouverture du clavier, puis fermer le clavier avec Entrée :

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

Remarques : keyboardHeight est la partie du clavier que la page doit encore dégager, pas la hauteur brute : dans l'application Android, la coque redimensionne la page pour le clavier, donc elle vaut 0 tant que le clavier est ouvert (l'événement d'affichage se déclenche quand même, et de nouveau quand la valeur change). Les événements de masquage ne se déclenchent que lorsque le clavier se ferme vraiment : ils permettent donc de suivre « le clavier est-il ouvert ». Les événements « Will » et « Did » se déclenchent ensemble. Pour le seul remplissage, onKeyboard((px) => …) est plus simple.

appmintNative.launchUrl appmintNative#

launchUrl(): Promise<string>

Exemple

Donne le lien qui a lancé ou repris l'application - un lien profond, un raccourci d'icône, un appui sur widget, une notification - pour que l'application puisse ouvrir le bon écran.

Retourne : Promise<string> - l'URL complète, ou '' quand l'application a été ouverte normalement. Toujours '' sur le web, où la barre d'adresse est déjà la route. Nécessite : rien pour les raccourcis et les appuis sur widget ; les liens externes nécessitent l'option Liens profonds lors de la compilation.

Routez dessus une seule fois quand l'application 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]);
}

Associez-le à onDeepLink() pour les liens qui arrivent alors que l'application est déjà ouverte :

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

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

ConsidĂ©rez la chaĂźne vide comme « ouverte normalement » et ne choisissez l'Ă©cran de dĂ©part Ă partir du lien que s'il y en a un. Un raccourci tel que appmint-shortcut://<package>/orders contient la route dans son chemin.

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

Remarques :

  • La valeur est CONSERVÉE, non consommée : un composant monté tardivement la voit encore. Un lien plus récent la remplace.
  • Elle est lue depuis la coque (WebToApk.getLaunchUrl()) ; n'appelez jamais cela directement dans une application créée par l'IA. Les raccourcis et les appuis sur widget arrivent sous la forme appmint-shortcut://<package><route>.
  • Analysez avec new URL(u) et naviguez vers son chemin ; ne chargez pas l'URL elle-mĂȘme.

appmintNative.listCallLog appmintNative#

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

Exemple

Lit le journal des appels, du plus récent au plus ancien. Application Android uniquement.

Retourne : Promise<CallLogEntry[]> - chaque { number, name, type, date, duration } ; type vaut incoming, outgoing, missed, voicemail, rejected, blocked ou other, date est en millisecondes epoch, duration en secondes. Vide quand le journal des appels est désactivé ou que l'autorisation est refusée. Nécessite : l'interrupteur Journal des appels (lecture) à l'étape 4 (Accès) - la compilation l'active quand votre code l'appelle - et l'autorisation de l'utilisateur à la première utilisation.

Appels manqués des 7 derniers jours.

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

Temps de conversation total par contact à partir des appels sortants et entrants, du plus long au plus court.

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

Un écran qui explique la liste vide, car « aucun appel » et « autorisation refusée » se ressemblent.

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

Remarques : limit est limitĂ© Ă 2000 par l'application. Une liste vide ne permet pas de distinguer « aucun appel » de « autorisation refusĂ©e ». can('callLog') vaut 'none' sur le web. Le journal d'appels est une autorisation restreinte de Google Play : vous avez besoin d'une dĂ©claration d'autorisations approuvĂ©e dans Play Console, sinon l'application est rejetĂ©e.

appmintNative.listContacts appmintNative#

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

Exemple

Lit le carnet d'adresses, page par page. Application Android uniquement, via le pont de l'enveloppe native.

Renvoie : Promise<Contact[]>. Un tableau vide quand il n'y a aucun contact, quand l'accÚs est refusé ou quand Contacts n'a pas été activé à la compilation. Nécessite : l'interrupteur Contacts à l'étape 4 (Access) - la compilation l'active quand votre code appelle ceci - et l'autorisation de l'utilisateur à la premiÚre utilisation.

Un Ă©cran de liste de contacts. Affichez-le seulement lĂ oĂč il peut fonctionner. Chaque contact est { 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
  }
}

Cherchez dans tout le carnet d'adresses en le lisant page par page dans une seule liste.

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

Associez-le à sendSms pour envoyer un message au premier numéro d'un contact.

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

Remarques : un rĂ©sultat vide ne permet pas de distinguer « aucun contact » de « autorisation refusĂ©e » - affichez une courte astuce (« Autorisez les contacts dans les ParamĂštres ») quand la liste est vide. Les contacts sont triĂ©s de A Ă Z par nom ; limit est limitĂ© Ă 2000. Pour une seule personne, utilisez pickContact(), qui ne nĂ©cessite aucune autorisation. Les contacts sont des donnĂ©es personnelles : dĂ©clarez-les dans votre formulaire Data safety Play et votre politique de confidentialitĂ©.

appmintNative.listSms appmintNative#

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

Exemple

Lit les SMS de la boîte de réception, des envoyés ou des brouillons, du plus récent au plus ancien. Application Android uniquement.

Renvoie : Promise<SmsMessage[]> - chaque { id, address, body, date, read, box }. Vide quand les SMS sont désactivés, que l'autorisation est refusée ou que la boßte est vide. Nécessite : l'interrupteur SMS (read & send) à l'étape 4 (Access) - la compilation l'active quand votre code appelle ceci - et l'autorisation de l'utilisateur à la premiÚre utilisation.

Afficher les 20 derniers messages de la boîte de réception.

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

Messages non lus d'un seul expéditeur, du plus récent au plus ancien.

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

Lire les envoyés et les brouillons en une fois, et les trier ensemble par date.

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

Remarques : Une liste vide ne permet pas de distinguer « aucun message » de « autorisation refusée ». limit est limité à 500 par l'application. can('smsList') vaut 'none' sur le web. Les nouveaux messages entrants ne font pas partie de cet enrobage ; les pages écrites à la main les reçoivent via l'événement appmint:sms-received (voir listSms pour le pont brut). Le SMS est une autorisation restreinte de Google Play : il vous faut une déclaration d'autorisations approuvée dans la Play Console, normalement accordée seulement aux applications SMS par défaut.

appmintNative.liveProgress appmintNative#

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

Exemple

Une carte d'état persistante et silencieuse dans le volet de notifications Android, que vous mettez à jour sur place - une livraison, un envoi, un minuteur, une séance de sport. start, update, end.

Retourne : start / update → Promise<boolean> (false quand indisponible, quand les notifications ne sont pas autorisées, ou quand l'utilisateur a désactivé sa catégorie « État en direct ») ; end → Promise<void>. Nécessite : l'application Android, et l'autorisation de notification sur Android 13+. Sur le web, can('liveProgress') vaut 'none' - affichez-y le même état dans votre application.

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 minuteur en cours : startedAt affiche une horloge qui compte à partir de ce moment.

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

Gérez un refus. start répond false ; liveProgress.lastError dit pourquoi.

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

Remarques : le même id met à jour une seule carte ; des identifiants différents créent des cartes séparées (l'id par défaut est 'live'). L'utilisateur ne peut pas la balayer pour la fermer : appelez donc toujours end. Demandez d'abord l'autorisation de notification sur Android 13+ (Notification.requestPermission()) : sans elle, start répond false et rien ne s'affiche.

appmintNative.LocalNotifications appmintNative#

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

Exemple

La forme @capacitor/local-notifications (LocalNotifications.schedule({ notifications })) pour le code écrit pour le plugin Capacitor. Une notification sans schedule s'affiche immédiatement (via notify()) ; une avec schedule: { at } est un rappel à cette heure, conservé après les redémarrages.

Retourne : Promise<{ notifications: { id }[] }>. Un rappel qui ne peut pas être programmé (navigateur, compilation sans Notifications, autorisation refusée) est rejeté avec la raison - il n'est jamais affiché plus tôt à la place. Rappels répétés : scheduleAlarm ou l'API de rappels. Nécessite : Android : Notifications activées à l'étape Accès (étape 4) lors de la compilation.

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

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

Plusieurs notifications en un appel, chacune remplaçant une précédente qui a le même 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' }] : []),
    ],
  });
}

Quand vous avez besoin de savoir si elle a été affichée, utilisez plutôt notify() et réagissez à la réponse.

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
}

Remarques : immédiat uniquement - les champs Capacitor id, schedule, at, actionTypeId et extra ne sont pas pris en charge ; chaque élément est { title, body?, icon?, tag? }. Pour une notification plus tard, une page écrite à la main utilise WebToApk.scheduleNotificationEx (Android). Pour les boutons, utilisez notifyWithActions.

appmintNative.lockOrientation appmintNative#

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

Exemple

Bloque l'Ă©cran dans une orientation pour une vue qui n'a de sens que comme ça - un plateau de jeu, un grand graphique, un pavĂ© de signature. 'any' libĂšre le verrouillage.

Retourne : Promise<boolean> - true quand le verrouillage (ou la libération) a été appliqué, false quand la plateforme a refusé. Ne rejette jamais. Nécessite : rien.

Verrouillez tant que la vue est ouverte, libérez en quittant :

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

Vérifier la réponse quand la vue en dépend :

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

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

Libérez le verrou quand l'écran disparaît, sous forme de hook partagé par plusieurs écrans.

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

Remarques :

  • Dans l'application, le plugin Capacitor ScreenOrientation applique le verrouillage.
  • Sur le web, elle utilise screen.orientation.lock(). La plupart des navigateurs la refusent hors plein écran et les navigateurs de bureau la refusent toujours : attendez-vous donc à false là-bas.
  • Donnez toujours à l'utilisateur un moyen de sortir d'une vue verrouillée.

appmintNative.longPress appmintNative#

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

Exemple

Vous appelle quand l'utilisateur maintient un doigt sur un élément - pour le menu contextuel d'une ligne, des réactions ou des actions de carte.

Retourne : une fonction de désabonnement - appelez-la au démontage. Nécessite : rien.

Options : ms - durée d'appui, 450 par défaut ; haptics - retour haptique moyen au déclenchement, par défaut true.

Ouvrez une feuille d'actions par appui long, avec un bouton visible pour le mĂȘme menu :

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

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

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

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

Un maintien plus lent, sans retour haptique :

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

L'associer à haptic pour une confirmation personnalisée, et nettoyer pour chaque ligne de liste.

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

Remarques :

  • Il se déclenche une fois par appui long et absorbe le clic qui suivrait, pour qu'un appui long ne soit pas aussi un « appui ».
  • Un déplacement de plus de 8 px l'annule, donc le défilement ne le déclenche jamais. Cela bloque aussi le menu d'appui long du navigateur sur cet élément.
  • Un appui long est invisible. Gardez toujours un bouton visible pour la même action.

appmintNative.markPushRead appmintNative#

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

Exemple

Marque un message de la boîte de réception AppMint Push comme lu, ou tous les messages quand il est appelé sans argument.

Renvoie : boolean de façon synchrone - true si quelque chose a changĂ©. Toujours false sur le web. NĂ©cessite : Android : AppMint Push activĂ© Ă l'Ă©tape Integrate (Ă©tape 3) lors de la compilation.

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

Comptez les messages non lus aprĂšs en avoir marquĂ© un, pour un badge dans votre propre en-tĂȘte.

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

Marquez tout comme lu quand l'utilisateur ouvre l'écran de la boßte de réception et actualisez quand un nouveau message arrive.

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

Remarques : l'état de lecture ne vit que sur ce téléphone ; la notification de la barre n'est pas supprimée.

appmintNative.mediaSession appmintNative#

mediaSession — @/lib/appmintNative

Exemple

Commandes de lecture sur l'écran de verrouillage et dans la notification pour une application qui lit de l'audio ou de la vidéo : publier ce qui est lu, recevoir les boutons Lecture / Pause / Suivant / Précédent / Arrêt, et envoyer une vidéo vers un téléviseur.

Renvoie : setMetadata, setPlaybackState, clear → void. onAction(handler) → une fonction de dĂ©sabonnement. cast(url) → Promise<boolean> (false quand rien n'a pu ĂȘtre proposĂ©). NĂ©cessite : Media Notification Ă l'Ă©tape 4 (Access) lors de la compilation ; les compilations IA l'activent pour vous quand le code utilise 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} />;
}

Envoyez la vidéo en cours vers une TV :

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

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

Effacez la carte de l'écran de verrouillage quand la lecture se termine, et ne touchez à rien quand la plateforme n'a pas de commandes multimédias.

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

Remarques :

  • Sur Android, les boutons passent par le navigator.mediaSession de l'application, installĂ© quand la page a fini de charger. Un onAction enregistrĂ© plus tĂŽt (par exemple dans un effet de montage) est perdu, enregistrez-le donc Ă la premiĂšre lecture.
  • L'action seek (avec positionMs) n'est transmise que sur le web ; la notification Android n'a pas de recherche de position.
  • artworkUrl doit être une adresse https:// complète sur Internet. Sur Android, cast(url) ouvre le sélecteur « Caster vers... » (un transfert ; elle se résout à true une fois le sélecteur demandé). Sur le web, elle utilise l'API Remote Playback.

appmintNative.Motion appmintNative#

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

Exemple

La forme @capacitor/motion sur les Ă©vĂ©nements standard devicemotion et deviceorientation : mouvement et inclinaison du tĂ©lĂ©phone. Le mĂȘme code fonctionne dans l'application et dans un navigateur.

Retourne : Promise<{ remove(): Promise<void> }> - appelez remove() pour arrêter. 'accel' rappelle avec { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval } ; 'orientation' avec { alpha, beta, gamma } (degrés). Nécessite : rien.

Secouer pour actualiser.

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

Inclinez une bulle de niveau avec l'événement d'orientation.

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

Forme hook avec nettoyage automatique pour un composant React qui lit les mouvements du téléphone.

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

Remarques : Certains navigateurs demandent d'abord l'accès au mouvement : lancez-la donc depuis un appui ; si l'utilisateur refuse, le handle est retourné mais aucun événement n'arrive. can('motion') vaut 'none' là où le navigateur n'a pas de DeviceMotionEvent. Les valeurs de capteur manquantes arrivent sous la forme 0. Appelez toujours remove() à la fermeture de l'écran - les événements de mouvement se déclenchent plusieurs fois par seconde.

appmintNative.musicVolume appmintNative#

musicVolume() — @/lib/appmintNative

Exemple

Lit le niveau de la musique de fond en boucle de l'application, de 0 à 100.

Retourne : number, de façon synchrone : 0-100, ou -1 quand il n'y a pas de musique (sur le web, ou une application compilée sans fichier musical). Nécessite : un fichier choisi sous Musique d'ambiance à l'étape 3 (Intégrer) lors de la compilation.

Affichez un curseur de musique seulement quand il y a de la musique :

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

Gardez le niveau synchronisĂ© avec un interrupteur « Couper la musique », en mĂ©morisant le volume Ă rĂ©tablir.

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

Baisser la musique pendant la lecture d'une vidéo, puis la rétablir.

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

Remarques : Application Android uniquement ; le web répond -1. Un niveau défini avec setMusicVolume est conservé pour le prochain lancement ; tant que la page n'en définit pas, c'est le niveau de la compilation.

appmintNative.navEnd appmintNative#

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

Exemple

Documenté avec appmintNative.navStart : l'exemple qui s'y trouve illustre aussi celui-ci.

navStart() affiche la fine barre de chargement en haut de l'application pour un changement d'écran que l'application ne peut pas voir d'elle-même ; navEnd() la termine.

Retourne : rien (les deux sont synchrones). Nécessite : rien. Sur le web (sans barre de la coque de l'application), les deux ne font rien.

Utilisez-les uniquement pour une attente où rien dans le DOM ne change tant que les données n'arrivent pas, et associez-les toujours à 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
  }
}

Enveloppez un envoi qui prend du temps, pour que l'utilisateur voie la barre pendant que le serveur travaille.

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

Créez un petit utilitaire qui enveloppe n'importe quelle promesse.

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

Remarques : Elles appellent WebToApk.navStarted() / navSettled(). L'application pilote déjà la barre quand votre application change d'écran dans le DOM : la plupart des changements de route n'ont donc besoin de rien. Rien n'est affiché pour un changement de moins de 200 ms environ, et une barre jamais terminée s'arrête d'elle-même après 2,5 secondes. Continuez à afficher votre propre spinner ou squelette ; la barre est un plus.

appmintNative.navStart appmintNative#

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

Exemple

navStart() affiche la fine barre de chargement en haut de l'application pour un changement d'écran que l'application ne peut pas voir d'elle-même ; navEnd() la termine.

Retourne : rien (les deux sont synchrones). Nécessite : rien. Sur le web (sans barre de la coque de l'application), les deux ne font rien.

Utilisez-les uniquement pour une attente où rien dans le DOM ne change tant que les données n'arrivent pas, et associez-les toujours à 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
  }
}

Enveloppez un envoi qui prend du temps, pour que l'utilisateur voie la barre pendant que le serveur travaille.

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

Créez un petit utilitaire qui enveloppe n'importe quelle promesse.

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

Remarques : Elles appellent WebToApk.navStarted() / navSettled(). L'application pilote déjà la barre quand votre application change d'écran dans le DOM : la plupart des changements de route n'ont donc besoin de rien. Rien n'est affiché pour un changement de moins de 200 ms environ, et une barre jamais terminée s'arrête d'elle-même après 2,5 secondes. Continuez à afficher votre propre spinner ou squelette ; la barre est un plus.

appmintNative.Network appmintNative#

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

Exemple

État en ligne/hors ligne sous la forme du plugin Network de Capacitor - le mĂȘme code que networkStatus() / onNetworkChange().

Retourne : getStatus() → Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) → la forme de Capacitor : une Promise du handle qui a aussi remove() lui-même ; le gestionnaire reçoit la même forme à chaque changement. Nécessite : rien.

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

Lisez le type actuel une fois et affichez-le dans une pastille d'état.

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

Retenez un gros téléchargement sur les données mobiles, et traitez aussi le cas hors ligne.

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

Remarques : Le nom de l'événement est ignoré - chaque écouteur reçoit les changements de connectivité. connectionType vaut 'wifi', 'cellular', 'none' ou 'unknown' : dans l'application, getStatus() et l'écouteur utilisent le plugin Network, sur le web navigator.onLine / navigator.connection.type. connected signifie que le téléphone a un réseau, pas que votre serveur répond.

appmintNative.networkStatus appmintNative#

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

Exemple

Indique, tout de suite, si l'appareil est en ligne.

Renvoie : { connected, type }, de façon synchrone. type vaut 'wifi', 'cellular', 'none' (hors ligne) ou 'unknown'. NĂ©cessite : rien.

Assez léger pour être appelé pendant le rendu :

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

Lire la valeur de départ, puis suivre les changements :

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

Choisissez une taille d'image plus légÚre sur données mobiles, en traitant aussi le cas 'none' (hors ligne).

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

Remarques :

  • Dans l'application, c'est la réponse du plugin Network (tenue à jour en arrière-plan, donc le tout premier appel peut encore lire la valeur propre à la WebView). Sur le web, c'est navigator.onLine plus navigator.connection.type lorsque le navigateur en fournit un, sinon 'unknown' - une classe de débit comme '4g' n'est jamais renvoyée comme type.
  • « Connecté » signifie que l'appareil a un réseau, pas que votre serveur répond. Gérez quand même les requêtes qui échouent.

appmintNative.nfc appmintNative#

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

Exemple

Des utilitaires à appel unique au-dessus de Web NFC (NDEFReader) : vérifier la prise en charge, lire des tags, écrire un tag texte. L'application Android utilise son propre NDEFReader ; sur le web, c'est le Web NFC propre à Chrome (Chrome sur Android).

Retourne : isSupported(): Promise<boolean> ; scan(onTag): Promise<() => Promise<void>> (la fonction d'arrêt), et onTag reçoit Array<{recordType, text, mediaType?}> par tag - les noms de type de Web NFC ('text', 'url', 'mime', …) et le texte décodé sur toutes les plateformes ; write(text): Promise<boolean> (true une fois un tag écrit, false en cas d'échec). Nécessite : sur Android, activez Prise en charge de Web NFC à l'étape 4 (Accès) lors de la compilation. scan() lève une exception quand il n'y a pas de NDEFReader - vérifiez d'abord 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>
  );
}

Écrivez un lien sur un tag, et dites Ă l'utilisateur pourquoi cela a Ă©chouĂ© le cas Ă©chĂ©ant.

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

Lire un tag, puis arrêter l'analyse.

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

Remarques : sur Android, isSupported() vaut true dĂšs que la compilation inclut le NFC - cela ne dit pas si le NFC est activĂ© (window.AndroidNFC.startScan() le dit). AprĂšs la fonction d'arrĂȘt, onTag n'est plus jamais appelĂ©. Sur Android, l'application continue elle-mĂȘme de capter les tags quand elle est au premier plan (pour qu'un appui n'ouvre pas une autre application), elle cesse seulement de vous les transmettre. Le text d'un enregistrement texte n'a pas de prĂ©fixe de langue ; le text d'un enregistrement URL est l'URL complĂšte. write() attend qu'un tag soit touchĂ©. Pour Ă©crire des enregistrements URL ou mime, et pour verrouiller un tag, utilisez directement NDEFReader.

appmintNative.notify appmintNative#

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

Exemple

Affiche une notification immédiatement depuis une application créée par IA, dans l'application Android et sur le web.

Retourne : Promise<boolean> - true une fois publiée, false quand l'utilisateur a refusé l'autorisation de notification, que les notifications sont désactivées pour l'application, ou qu'elles ne sont pas disponibles ici. Elle demande elle-même l'autorisation la première fois (Android 13+ et le web). Nécessite : Android : Notifications activées à l'étape Accès (étape 4) lors de la compilation.

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

Demandez au bon moment : le premier notify() affiche la boîte de dialogue d'autorisation. Pour demander depuis votre propre bouton « Activer les alertes » - avant que quoi que ce soit ne soit publié - utilisez l'API standard, que l'application prend aussi en charge :

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

Ouvrez le bon écran quand l'utilisateur touche la notification, en combinant notify avec 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;
}

Remarques : Android utilise le pont de notification de l'enveloppe (icon devient la grande icône ; utilisez une image https:// ou data:) ; le web utilise l'API Notification. Une fois que l'utilisateur a refusé définitivement, notify() se résout en false sans redemander - orientez-le vers les paramètres du téléphone. Pour des boutons, utilisez notifyWithActions ; pour une carte de progression persistante, utilisez liveProgress.

appmintNative.notifyWithActions appmintNative#

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

Exemple

Affiche une notification avec jusqu'à 3 boutons (« Terminé », « Reporter »). L'appui revient par onNotificationAction.

Retourne : Promise<boolean> - true une fois publiée, false quand l'autorisation de notification a été refusée (elle est demandée la première fois, comme notify()). Nécessite : Android : Notifications activées à l'étape Accès (étape 4) lors de la compilation, et l'autorisation de l'utilisateur sur Android 13+. Sur le web, can('notificationActions') est '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 minuteur est terminé : un bouton ajoute cinq minutes, l'autre l'arrête.

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

Gérez une autorisation refusée après l'appel, et nettoyez dans un composant.

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

Remarques : Chaque bouton affiche son title ; son id est ce qui revient sous la forme e.actionId. L'option input (champ de réponse intégré) n'est pas prise en charge dans cette application - elle est ignorée, le bouton n'a pas de champ de réponse et e.text vaut toujours ''. Un appui n'atteint la page que pendant que l'application s'exécute. Sur le web, l'enrobage publierait une notification simple sans boutons, c'est pourquoi l'exemple vérifie d'abord can().

appmintNative.offlineBanner appmintNative#

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

Exemple

Monte une bande toute prête qui glisse sous la barre d'état quand la connexion tombe, et affiche brièvement « De nouveau en ligne » à son retour.

Retourne : une fonction de désabonnement qui supprime la bannière et arrête l'écoute. Nécessite : rien.

Montez-le une fois au démarrage de l'application :

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

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

Vos propres mots (par exemple dans la langue de l'application) :

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

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

Montez la bannière uniquement pour la zone connectée de l'application, et retirez-la à la déconnexion.

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

Remarques :

  • Par dĂ©faut : « Vous ĂȘtes hors ligne » et « De nouveau en ligne ». Le message en ligne disparaĂźt aprĂšs environ 1,8 seconde ; celui hors ligne reste jusqu'au retour de la connexion.
  • Si l'appareil est déjà hors ligne au montage, la bannière s'affiche immédiatement.
  • Construit sur onNetworkChange(), il est donc rĂ©el sur toutes les surfaces. Les couleurs viennent des jetons de l'application (--ink / --bg hors ligne, --accent / --accent-ink en ligne). Il a role="status" pour les lecteurs d'Ă©cran.
  • Montez-le une seule fois. Deux appels dessinent deux bannières.

appmintNative.onAlarm appmintNative#

onAlarm(cb) — @/lib/appmintNative

Exemple

Écoute les résultats d'alarme : l'application ouverte par une alarme (action: 'open', avec ringing), et la façon dont une sonnerie s'est terminée - dismiss, snooze, missed (a sonné jusqu'au bout, ou remplacée par une autre alarme) ou error. Les résultats survenus pendant que l'application était fermée sont livrés au prochain chargement de la page.

Renvoie : une fonction qui arrĂȘte l'Ă©coute. La fonction de rappel reçoit { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. NĂ©cessite : 🔔 Alarm clock Ă l'Ă©tape Access (Ă©tape 4) lors de la compilation.

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

Programmez une alarme, en répondant d'abord à 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.';
}

Arrêter ou reporter l'alarme qui sonne depuis votre propre écran, et réagir au résultat.

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

Remarques : les rappels ordinaires qui ouvrent l'application arrivent aussi ici avec action: 'open' et ringing: false.

appmintNative.onBackGesture appmintNative#

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

Exemple

Permet à une feuille, un modal, une visionneuse ou une étape d'assistant ouverts de répondre en premier au geste de RETOUR - le balayage du bord / bouton retour d'Android dans l'application, le bouton retour du navigateur sur le web.

Retourne : une fonction de désabonnement - appelez-la à la fermeture ou au démontage de la vue. Votre gestionnaire retourne true pour consommer l'appui (vous avez fermé quelque chose) ou false pour le laisser passer. Nécessite : rien.

Enregistrez-vous uniquement pendant que la feuille est ouverte :

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

Revenez en arrière dans un assistant, et laissez le retour de la dernière étape quitter normalement :

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

Fermez une visionneuse d'image avec le geste de retour, en liant l'enregistrement à l'ouverture de l'image.

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

Remarques :

  • Les gestionnaires forment une pile : le dernier enregistrĂ© (la feuille au-dessus) est interrogĂ© en premier. Un gestionnaire qui lĂšve une erreur ne consomme rien.
  • Sur Android, cela passe par le gestionnaire de retour de la coque. Sans lui, balayer pour revenir avec une feuille ouverte ferme toute l'application.
  • Sur le web, il ajoute une entrée d'historique tant qu'un gestionnaire est enregistré et la retire quand le dernier se désabonne.
  • Affichez quand mĂȘme un contrĂŽle de retour / fermeture visible : beaucoup d'utilisateurs n'utilisent jamais le geste de retour.
  • React Router gère déjà le retour de page normal ; n'utilisez ceci que pour un état que le routeur ne connaît pas. Désabonnez-vous toujours, sinon une feuille fermée continue d'absorber les appuis sur Retour.

appmintNative.onInAppMessage appmintNative#

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

Exemple

Vous appelle pour chaque message intégré (les campagnes du créateur) qui est affiché, touché, ignoré ou répondu sur cette installation. Retourne la fonction de désabonnement.

Retourne : () => void. Le callback reçoit {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. Ne se déclenche jamais sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

Afficher les messages vous-même (avec setInAppPresentation('none'), presented vaut false et message porte le design) :

import { useEffect, useState } from 'react';
import { onInAppMessage, setInAppPresentation, type InAppMessageEvent } from '@/lib/appmintNative';

export function useOwnInAppDialog() {
  const [msg, setMsg] = useState<InAppMessageEvent['message'] | null>(null);
  useEffect(() => {
    setInAppPresentation('none');
    const off = onInAppMessage((e) => { if (e.type === 'shown' && !e.presented && e.message) setMsg(e.message); });
    return () => { off(); setInAppPresentation('auto'); };
  }, []);
  return { msg, close: () => setMsg(null) };
}

Réagissez à un bouton touché par l'utilisateur dans le dessin propre à l'application :

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

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

Remarques : button vaut -1 pour un appui sur le message lui-mĂȘme, sinon l'indice du bouton.

Réponses (type: 'answered'). Un message avec une rangée d'étoiles (Sondage par étoiles, Notez-nous) ou des pastilles de score de 0 à 10 (Score NPS) signale la réponse une fois, à sa fermeture : question vaut 'stars' (valeur 1-5) ou 'nps' (valeur 0-10), et template nomme le modèle de la galerie dont il vient. Le centre d'envoi affiche la moyenne ou le score NPS ; votre code peut y donner suite :

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

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

Gardez votre propre trace des notes en étoiles :

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

Remercier les personnes qui ont donné une bonne note, dans un composant 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

Exemple

Indique combien de pixels de la page le clavier à l'écran recouvre, à chaque changement, pour qu'une barre du bas puisse se placer au-dessus.

Renvoie : une fonction de dĂ©sabonnement - appelez-la au dĂ©montage. La fonction de rappel reçoit un nombre en pixels CSS : la marge basse supplĂ©mentaire dont vous avez besoin, 0 quand le clavier est fermĂ©. NĂ©cessite : rien.

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

Garder une liste de discussion défilée sur le message le plus récent pendant l'ouverture du clavier :

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 réutilisable, avec une marge de zone sûre qui disparaît tant que le clavier est ouvert :

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>

Remarques :

  • Dans l'application, elle écoute le plugin Capacitor Keyboard et ne signale que la partie du clavier que la mise en page n'a pas déjà absorbée. La coque redimensionne généralement la page pour le clavier : la valeur est donc souvent 0 - c'est normal ; n'ajoutez pas vous-même la hauteur brute du clavier.
  • Dans une application en plein Ă©cran ou en mode kiosque (page non redimensionnĂ©e), elle indique la hauteur totale du clavier.
  • Sur le web, elle utilise visualViewport, appelle le callback une fois tout de suite, et considère moins de 80 px comme de l'interface du navigateur (signalée comme 0).
  • Dans l'application, le premier appel arrive à la première ouverture du clavier : initialisez donc votre état à 0.

appmintNative.onNetworkChange appmintNative#

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

Exemple

Vous appelle chaque fois que la connexion tombe ou revient - pour une bannière hors ligne, ou pour relancer des écritures en attente.

Retourne : une fonction de désabonnement - appelez-la au démontage. Nécessite : rien.

Relancez le travail en file d'attente quand l'appareil est de nouveau en ligne :

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

Afficher l'état actuel tout de suite, puis suivre les changements (le callback n'est pas appelé à l'abonnement) :

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

Retenez les gros téléchargements sur données mobiles, et traitez 'none' comme hors ligne :

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

Remarques :

  • Elle ne vous appelle PAS avec l'Ă©tat actuel quand vous vous abonnez. Lisez networkStatus() pour la valeur de dĂ©part.
  • Dans l'application, elle Ă©coute le plugin Network de Capacitor (networkStatusChange) ; type vaut 'wifi', 'cellular', 'none' ou 'unknown'.
  • Sur le web, elle Ă©coute les Ă©vĂ©nements online / offline et signale networkStatus().
  • Pour une bannière prête à l'emploi, utilisez offlineBanner(), qui est construit dessus.

appmintNative.onNotificationAction appmintNative#

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

Exemple

Écoute les appuis sur les boutons de notification (depuis notifyWithActions).

Renvoie : une fonction qui arrĂȘte l'Ă©coute. La fonction de rappel reçoit { actionId, tag } - l'identifiant du bouton et le tag de la notification. L'application Android n'envoie aucun text de rĂ©ponse (la rĂ©ponse intĂ©grĂ©e n'est pas prise en charge), ne vous fiez donc pas Ă e.text. NĂ©cessite : la mĂȘme chose 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]);
}

Événement brut dans une page écrite à la main (application Android) :

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

Publiez une notification avec des boutons seulement lĂ oĂč la compilation les prend en charge, et gĂ©rez l'appui d'aprĂšs le tag de la notification :

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

Remarques : dans l'application Android, c'est l'événement appmint:notification-action (aussi window.onAppMintNotificationAction(detail)) ; sur le web, il ne fait rien. Un appui n'est transmis que lorsque l'application est en cours d'exécution - toucher un bouton n'ouvre pas l'application.

appmintNative.onNotificationClick appmintNative#

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

Exemple

Exécute une fonction de rappel quand l'utilisateur touche une notification publiée par l'application - y compris l'appui qui a lancé l'application à froid - afin que l'application ouvre le bon écran.

Retourne : la fonction de désabonnement. Le callback reçoit { tag, title, body, data } - data est ce que vous avez passé à notify({ …, data }). Nécessite : les notifications, comme notify(). Événement sous-jacent : 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 } });
}

Ouvrir le bon écran après l'appui, même quand il a démarré l'application à froid, et ignorer les notifications qui ne portent pas de données :

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

Envoyer une notification uniquement là où les notifications existent, avec des données pour l'appui (le data nécessite 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');
}

Remarques : L'appui traverse l'écran de démarrage et l'écran du code PIN jusqu'à la page. data doit être du JSON (moins de 16 Ko) et nécessite un tag.

appmintNative.onPullToRefresh appmintNative#

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

Exemple

Ajoute le geste de tirer vers le bas pour actualiser à une liste défilante : tirez vers le bas en haut, relâchez, et vos données se rechargent.

Retourne : une fonction de désabonnement - appelez-la au démontage. Nécessite : rien.

el est l'élément qui défile (ou document.documentElement quand toute la page défile). Le spinner reste jusqu'à ce que la promesse de onRefresh soit résolue :

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

Une page qui défile dans son ensemble, avec une traction plus longue :

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

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

Actualiser une liste via un hook de données, avec un léger retour haptique quand les données sont arrivées :

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

Remarques :

  • Elle ne démarre que lorsque la liste est tout en haut. Le threshold par défaut est de 72 px de traction.
  • Il dessine un indicateur rond aux couleurs de l'application (--surface, --accent), déclenche un retour haptique moyen au point de validation, et ignore les nouveaux tirages pendant qu'une actualisation s'exécute.
  • Il actualise vos DONNÉES, pas la page. Le tirer-pour-actualiser de l'enveloppe qui recharge la page (Activer le tirer-pour-actualiser à l'étape 3 (Intégrer)) est désactivé par défaut - laissez-le ainsi quand vous utilisez ceci, pour qu'un tirage ne recharge pas aussi la page.
  • Construit sur les Pointer Events : identique sur toutes les surfaces. Il définit overscroll-behavior-y: contain sur el et le rétablit au désabonnement.

appmintNative.onShared appmintNative#

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

Exemple

Vous appelle quand une autre application partage du texte ou un lien vers cette application alors qu'elle est déjà ouverte.

Retourne : une fonction de désabonnement - appelez-la au démontage. Nécessite : l'option Recevoir les partages d'autres applications de la compilation avec « Texte et liens » - activée automatiquement quand votre code appelle onShared( ou sharedText(.

Gérez les deux cas - un partage qui a lancé l'application, et un qui arrive plus tard :

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

Une page Ă©crite Ă la main peut Ă©couter elle-mĂȘme l'Ă©vĂ©nement du shell :

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

PrĂ©remplissez un formulaire « nouvelle note » Ă partir d'un lien partagĂ©, seulement lĂ oĂč la compilation peut recevoir des partages :

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

Remarques :

  • Android : écoute l'événement appmint:shared de l'enveloppe ; l'enveloppe appelle aussi window.onAppMintShared(detail) quand une page la définit. La même charge est aussi conservée pour sharedText() jusqu'à ce que clearShared().
  • Sur le web, elle ne se déclenche jamais et retourne un désabonnement sans effet.

appmintNative.onVolumeChange appmintNative#

onVolumeChange(cb) — @/lib/appmintNative

Exemple

Vous appelle cb chaque fois qu'un des niveaux de volume du téléphone change - touches physiques, curseur système, autre application.

Retourne : une fonction qui arrête l'écoute (la dernière à s'arrêter coupe la surveillance native). Le callback reçoit { stream, current, max, percent, muted? } pour chaque flux qui a changé. Ne fait rien sur le web. Nécessite : rien.

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

Avertir quand le volume multimédia est mis à zéro pendant la lecture d'une vidéo, et arrêter l'écoute au démontage :

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

Reflétez le niveau du téléphone sur un curseur et réécrivez la modification de l'utilisateur (le web n'a pas de volume, donc getVolume vaut null là-bas) :

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>

Exemple

Ouvre l'écran des paramètres de notification propre à cette application (application Android).

Retourne : Promise<boolean> - vrai quand l'écran s'est ouvert ; faux sur le web et partout où cet écran n'existe pas. Vérifiez can('notificationSettings') avant d'afficher le contrôle. Nécessite : rien.

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

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

Remarques : pour un « Activer les notifications » en un seul appui, utilisez enableNotifications(), qui n'ouvre cet Ă©cran que quand la boĂźte de dialogue d'autorisation ne peut plus ĂȘtre affichĂ©e.

appmintNative.passkeys appmintNative#

passkeys — available / create / get / origin / assetLinks

Exemple

Connexion et enregistrement par clé d'accÚs (passkey) avec les options JSON que produit votre serveur de passkeys. Dans l'application Android, elle utilise le gestionnaire de passkeys d'Android ; sur le web, elle utilise navigator.credentials.

Renvoie : available() → 'native' | 'web' | 'none' ; create(optionsJSON) → Promise du RegistrationResponseJSON ; get(optionsJSON) → Promise de l'AuthenticationResponseJSON (envoyez l'un ou l'autre en POST Ă votre serveur). Ils rejettent avec une DOMException (NotAllowedError, InvalidStateError, SecurityError, NotSupportedError). origin() → chaĂźne, assetLinks() → tableau (application uniquement ; '' / [] sur le web). NĂ©cessite : un serveur de passkeys ; dans l'application aussi Android 9+ avec les services Google Play et le 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),
  });
}

Valeurs de configuration unique, depuis l'application installée via 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

Proposer le bouton de passkey seulement là où il peut fonctionner, et le dire quand ce n'est pas possible :

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

Remarques : sur le web, il faut une page sécurisée (https://). Une page incluse dans l'application n'a pas de domaine : les options de votre serveur doivent définir rp.id / rpId sur votre domaine. Le remplissage automatique des passkeys (mediation: 'conditional') n'est pas disponible dans l'application - démarrez depuis un bouton.

appmintNative.paste appmintNative#

paste(): Promise<string>

Exemple

Lit le texte du presse-papiers du système.

Retourne : Promise<string> - le texte du presse-papiers, ou '' quand le presse-papiers est vide. Il REJETTE quand ce navigateur ne peut pas du tout lire le presse-papiers, ou quand l'utilisateur refuse l'autorisation. Nécessite : rien.

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 forme à la façon de Capacitor :

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

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

Collez dans une zone de texte avec un message pour chaque résultat, et associez-le à 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');
}

Remarques : Dans l'application, le plugin Clipboard lit le texte ; un presse-papiers vide donne '', pas une erreur. Dans un navigateur, c'est navigator.clipboard.readText() (Chromium uniquement), qui demande l'autorisation à l'utilisateur - appelez-la depuis un appui.

appmintNative.pickContact appmintNative#

pickContact(): Promise<Contact | null>

Exemple

Ouvre le sélecteur de contacts du système pour que l'utilisateur choisisse une personne : le sélecteur d'Android dans l'application, l'API Contact Picker dans Chrome sur Android.

Renvoie : Promise<Contact | null> - null quand l'utilisateur annule ou que l'accÚs est refusé. Nécessite : rien (le sélecteur du systÚme ne demande aucune autorisation).

Choisissez une personne et remplissez un formulaire. Le contact a la mĂȘme forme dans l'application et sur le 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 bouton « Choisir un contact » qui affiche le choix, et une simple zone de numéro là où le sélecteur n'existe pas :

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

Transmettez le numéro choisi à l'application de SMS, avec un message quand le contact n'en a pas :

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

Remarques : Dans l'application, seul le numéro touché revient, jamais les e-mails, et photoUri est une adresse content:// de la photo du contact. can('contactPick') vaut 'none' dans les navigateurs sans l'API Contact Picker (bureau, Safari) - n'affichez pas le bouton là-bas.

appmintNative.pickFile appmintNative#

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

Exemple

Permet à l'utilisateur de choisir un fichier et de le lire comme texte. Dans l'application, il ouvre le sélecteur de documents d'Android ; sur le web, il ouvre un champ de fichier normal.

Renvoie : Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }, ou null seulement quand l'utilisateur annule. Elle rejette avec une Error dont le code est 'no-picker' (le téléphone n'a pas d'application de sélection de documents), 'disabled' ou 'failed' quand aucun sélecteur n'a pu s'ouvrir. text() lit tout le fichier et rejette avec le code du pont (par exemple 'not-found') quand c'est impossible. Nécessite : rien. Avec Native Folder Access (SAF) coché à l'étape 4 (Access), l'application utilise le sélecteur de documents d'Android ; sans cela, elle utilise le sélecteur de fichiers de la page.

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

Importer une sauvegarde JSON et gérer chaque façon dont pickFile peut échouer :

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

Choisir un fichier texte et le mettre dans un éditeur, à côté des détails du fichier :

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

Remarques : Dans l'application, text() lit le fichier par morceaux de 4 Mo jusqu'à la fin et le décode en UTF-8, donc les gros fichiers sont lus en entier - gardez votre propre limite de taille pour ce que l'écran peut gérer. Un sélecteur qui ne peut pas s'ouvrir rejette, enveloppez donc aussi pickFile() dans un try/catch. Sur le web, text() est le File.text() propre au navigateur. Pour les fichiers binaires ou l'écriture en retour, les pages écrites à la main utilisent WebToApkFS.

appmintNative.pip appmintNative#

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

Exemple

Garde une vidéo en lecture dans une petite fenêtre flottante pendant que l'utilisateur fait autre chose.

Renvoie : enter() renvoie Promise<boolean> - true quand la fenĂȘtre flottante s'est ouverte. exit() renvoie Promise<void>. Aucun ne rejette. NĂ©cessite : dans l'application Android, l'option Picture-in-Picture (PiP) de la compilation Ă l'Ă©tape 3 (Integrate) - elle s'active automatiquement quand votre code appelle pip.enter(.

Un bouton « réduire » sur un lecteur :

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

Quittez-le quand le lecteur se ferme (web) :

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

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

Passer automatiquement en picture-in-picture quand l'utilisateur quitte une vidéo en lecture, seulement là où c'est pris en charge :

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

Remarques :

  • Android (8+) : TOUTE l'application se rĂ©duit dans la fenĂȘtre flottante, et l'argument video est ignorĂ©. Elle renvoie false sur un Android plus ancien ou dans une compilation sans l'option PiP. L'utilisateur agrandit la fenĂȘtre pour revenir ; exit() n'y fait rien.
  • Le web : le picture-in-picture propre Ă l'Ă©lĂ©ment <video>. La vidĂ©o doit ĂȘtre chargĂ©e (gĂ©nĂ©ralement en lecture) ; un appel sans video renvoie false.
  • Appelez-la depuis un appui.

appmintNative.platform appmintNative#

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

Exemple

Indique sur quelle surface le code s'exécute : l'application Android installée ou un navigateur web.

Retourne : une chaîne, immédiatement : 'android' dans l'application, 'web' dans un navigateur. Il interroge d'abord Capacitor (Capacitor.getPlatform()), puis vérifie le pont de l'enveloppe Android, sinon 'web'. Le type liste aussi 'ios', mais une application AppMint ne l'obtient jamais - AppMint ne compile que des applications Android. Nécessite : rien.

Choisissez le bon lien et copiez.

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

Afficher des contrôles réservés à l'application.

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

Étiqueter les événements d'analyse avec la surface.

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

Remarques : platform() ne répond jamais 'web' dans l'application installée. Ne l'utilisez pas pour décider si une fonctionnalité existe - beaucoup dépendent aussi des options de compilation ; can('<capability>') répond à cela.

appmintNative.privacyScreen appmintNative#

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

Exemple

Bloque les captures d'écran, l'enregistrement d'écran et l'aperçu du sélecteur d'applications tant qu'un écran contenant des secrets est ouvert.

Retourne : enable() → Promise<boolean> (false là où rien ne peut le bloquer) ; disable() → Promise<void> ; isEnabled() → Promise<boolean>. Nécessite : rien. Dans l'application Android, il utilise le blocage de capture d'écran de l'enveloppe (captures, enregistrement d'écran, diffusion et miniature des récents).

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

Dites à l'utilisateur ce qui est protégé - uniquement quand c'est vraiment le cas :

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

Activez la protection pour une vue sensible et dites à l'utilisateur si elle n'a pas pu être appliquée :

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

Remarques : Dans l'application, le blocage appartient à la page actuelle et est levé quand l'application navigue vers une autre route, réactivez-le donc quand cet écran s'affiche à nouveau. Sur le web, can('privacyScreen') vaut 'none' et enable() retourne false - n'y prétendez pas protéger. Rien n'empêche un appareil photo pointé sur l'écran.

appmintNative.pushInbox appmintNative#

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

Exemple

Retourne les messages AppMint Push reçus par cette installation (les plus récents d'abord, jusqu'à 50), pour un écran de notifications dans l'application.

Retourne : PushMessage[] de façon synchrone : { id, title, body, imageUrl, link, receivedAt, read }. Un tableau vide sur le web et quand AppMint Push est désactivé. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

Affichez un compteur de non-lus qui suit les nouveaux arrivages :

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

Afficher la liste, avec un état vide pour le web et pour les compilations sans AppMint Push, et un bouton « Tout marquer comme lu » :

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

Remarques : la boîte de réception est la vérité enregistrée - lisez-la au montage ; l'événement appmint:push ne se déclenche que lorsque l'application est à l'écran. Les messages ignorés sont exclus.

appmintNative.readBarcodeFrom appmintNative#

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

Exemple

Lit les codes-barres et les codes QR d'une image que la page possède déjà - un fichier photo, un Blob, un <img>, un <canvas> ou une image vidéo. Aucune caméra n'est ouverte.

Retourne : Promise<{ value: string; format: string }[]> - tous les codes trouvés, dans l'ordre du détecteur ; un tableau vide s'il n'y en a pas, si l'image est illisible ou si la lecture de codes-barres n'est pas disponible. Ne rejette jamais. Nécessite : rien.

Lisez un code depuis une photo choisie par l'utilisateur :

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

À partir d'une image déjà présente sur la page :

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

Lisez un code depuis une image de canvas, en vérifiant d'abord ce que l'appareil prend en charge :

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

Remarques :

  • Utilise le BarcodeDetector du moteur du navigateur (présent dans la WebView de l'application Android ; absent de Safari et de nombreux navigateurs de bureau - il retourne alors []).
  • Un simple <input type="file" accept="image/*"> ouvre le sélecteur de photos d'Android et ne nécessite aucune autorisation de stockage.
  • Restreignez formats quand vous les connaissez : c'est plus rapide et il y a moins d'erreurs de lecture. barcodeFormats() liste ce que l'appareil prend en charge.

appmintNative.readOtp appmintNative#

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

Exemple

Attend le code SMS à usage unique pour cette application et le retourne - la feuille de consentement SMS de Google dans l'application Android, WebOTP dans Chrome sur le web.

Retourne : Promise<string | null> - le code, ou null quand l'utilisateur a refusé, que cinq minutes se sont écoulées ou que signal a interrompu. Lève une exception quand can('otp') vaut 'none'. Nécessite : rien à activer. Le SMS doit se terminer par @<host> #<code> (dans l'application Android, l'hôte est 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" />
  );
}

Gérer la réponse « aucun code », et proposer un renvoi quand l'attente se termine sans code :

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

Interceptez l'exception quand vous ne pouvez pas vérifier can('otp') d'abord :

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

Remarques : c'est un accélérateur - gardez toujours le champ saisissable au clavier.

appmintNative.recognizeText appmintNative#

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

Exemple

Lit le texte d'une image sur l'appareil (can('ocr')) : le module de reconnaissance ML Kit de l'application Android, ou le TextDetector de Chrome là où il est activé.

Renvoie : Promise de { text, blocks: [{ text, box, lines: [{ text, box }] }] }, cadres en pixels d'image. Rejette avec code 'unavailable' lĂ oĂč can('ocr') vaut 'none', ou une DOMException quand la reconnaissance Ă©choue. NĂ©cessite : rien, aucune autorisation.

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

Gérez explicitement les cas « aucun module de reconnaissance » et d'échec :

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

Lire une écriture autre que le latin, et lister chaque ligne avec sa position :

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

Remarques : écritures latines uniquement. Le premier appel sur un téléphone neuf peut prendre quelques secondes, le temps que les services Play récupÚrent le modÚle - affichez une progression.

appmintNative.remoteConfig appmintNative#

remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()

Exemple

Affiche le bandeau d'annonce et l'Ă©cran de maintenance que le crĂ©ateur publie depuis Remote Update - pour la page quand elle s'exĂ©cute dans un navigateur web. Dans l'application Android, la coque le fait dĂ©jĂ elle-mĂȘme : ces appels n'y font donc rien.

Retourne : start(manifestUrl?) → Promise<void> (récupération + affichage ; ne rejette jamais) ; fetch(url) → Promise<RemoteConfig | null> (ne rejette jamais ; null = rien ne s'applique) ; apply(cfg) → une fonction de nettoyage ; manifestUrl() → string (le content de <meta name="appmint-remote-manifest">, '' s'il n'y en a pas) ; handledHere() → boolean ; dismissed(id) → boolean ; dismiss(id) → void. Nécessite : rien dans l'application - la mise à jour à distance y est native.

Peut ĂȘtre appelĂ©e sans risque au dĂ©marrage ; dans l'application, elle revient aussitĂŽt :

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

// Your Remote Update manifest URL. AppMint does not write the meta tag into the page,
// so pass the URL when the same page also runs on the web.
const MANIFEST = 'https://example.com/remote-update/manifest.json';

export function App() {
  useEffect(() => { void remoteConfig.start(MANIFEST); }, []);   // does nothing in the Android app
  return <main>{/* … */}</main>;
}

Ou lire les valeurs et dessiner votre propre interface (web uniquement) :

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)

Affichez vous-mĂȘme le texte de maintenance sur le web, et laissez l'application avoir son propre Ă©cran :

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

Remarques :

  • RemoteConfig = { version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }.
  • handledHere() vaut false dans l'application Android : le shell lit le manifeste nativement et dessine lui-mĂȘme l'annonce, vous ne voyez donc jamais deux banniĂšres.
  • start() sans argument utilise manifestUrl(). Une compilation AppMint n'ajoute pas cette balise meta, donc sans argument (ou votre propre <meta name="appmint-remote-manifest" content="…">), rien n'est récupéré.
  • fetch() renvoie null pour une publication en pause ou annulée, un manifeste dont la liste platforms n'inclut pas web, un format de manifeste plus récent, ou toute erreur réseau.
  • Le mode maintenance l'emporte : apply() recouvre alors toute la page avec le texte de maintenance. Une annonce ignorée reste masquée jusqu'à la publication d'un nouveau announcementId.

appmintNative.requestDndAccess appmintNative#

requestDndAccess() — @/lib/appmintNative

Exemple

Demande l'accÚs Ne pas déranger, qu'Android exige avant que l'application puisse changer le volume de la sonnerie ou des notifications pendant que Ne pas déranger est activé.

Retourne : 'granted', 'opened' (l'écran des paramètres a été affiché - revérifiez au retour de l'utilisateur), 'not_declared', ou 'unavailable' sur le web. Nécessite : rien à cocher - l'appeler dans votre code fait que la compilation déclare l'autorisation.

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

Demandez une fois, puis réessayez le changement de volume quand l'utilisateur revient des paramètres :

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

Gérer chaque réponse possible :

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>

Exemple

Demande à Google Play d'afficher sa propre feuille de notation dans l'application (Play In-App Review).

Retourne : Promise<boolean> - true uniquement quand Play a affiché la feuille ; false sur le web, pour une installation qui ne vient pas de Google Play, quand Play a refusé (son quota) ou a échoué. Utilisez askForReview() pour savoir lequel. Nécessite : rien ; la feuille n'apparaît que pour une application installée depuis 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();
  }
}

Découvrez pourquoi la feuille ne s'est pas affichée, sans renvoyer l'utilisateur ailleurs :

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

Ne demandez qu'aprÚs un moment de réussite, et au plus une fois par mois :

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

Remarques : Google Play limite le débit de la feuille et n'affiche souvent rien - ne récompensez jamais un avis et ne créez jamais de bouton « Notez-nous » qui attend l'ouverture de la feuille (utilisez askForReview({ openStore: true }) pour un bouton). Il ne quitte jamais l'application pour la fiche Play.

appmintNative.restorePushMessage appmintNative#

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

Exemple

Annule dismissPushMessage(id) - le message revient dans la boîte de réception AppMint Push à son ancienne place.

Retourne : boolean de façon synchrone - true si elle a été restaurée ; false si l'identifiant est inconnu ou n'a pas été écarté (toujours false sur le web). Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

Proposer Annuler juste aprÚs un balayage de suppression, dans une petite ligne 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>
  );
}

Vérifiez la réponse, car un identifiant inconnu ou jamais masqué donne 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)`;
}

Remarques : les messages supprimés avec clearPushInbox() ou retirés par vous ne peuvent pas être restaurés.

appmintNative.safeArea appmintNative#

safeArea(): { top: number; right: number; bottom: number; left: number }

Exemple

Donne l'espace occupé par la barre d'état, l'encoche de la caméra et la barre de navigation, en pixels CSS, pour que les barres fixes ne se retrouvent pas dessous.

Renvoie : { top, right, bottom, left } des nombres, de façon synchrone. NĂ©cessite : rien.

La plupart du temps, le CSS suffit - l'exécution place --safe-top, --safe-right, --safe-bottom, --safe-left sur :root:

export function BottomNav() {
  return (
    <nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
      {/* tabs */}
    </nav>
  );
}

Utilisez les nombres quand le code doit faire le calcul, par exemple pour placer un bouton flottant :

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

Garder une liste défilante à distance des barres en dimensionnant son remplissage à partir des valeurs :

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

Remarques :

  • Mesuré à partir de env(safe-area-inset-*) en CSS. La valeur est mise en cache et re-mesurée après un redimensionnement ou une rotation de la fenêtre.
  • Avant que document.body n'existe (très tôt au moment de l'import), il retourne des zéros - appelez-le depuis un effet.
  • Sur un navigateur de bureau, les quatre sont gĂ©nĂ©ralement 0.

appmintNative.saveFile appmintNative#

saveFile(fileName, data, mime?) from '@/lib/appmintNative'

Exemple

Enregistre du texte ou un Blob sous forme de fichier que l'utilisateur conserve, avec le nom de votre choix. Dans l'application, elle ouvre la feuille « Enregistrer sous… » d'Android avec ce nom ; dans un navigateur, c'est un téléchargement normal.

Retourne : (synchrone) boolean - true quand le téléchargement a démarré, false quand il n'y a pas de document ou que sa création a échoué. Il ne dit pas si l'utilisateur a enregistré ou annulé la feuille. Nécessite : rien.

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 garde son propre type (le troisiÚme argument est ignoré) :

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

const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
saveFile('Backup.json', blob);

Enregistrez le résultat d'une récupération, et dites-le quand elle n'a pas pu démarrer :

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

Remarques : C'est un simple <a download> en coulisses, que l'application transforme en feuille « Enregistrer sous… » (voir downloads). Passez toujours un nom avec une extension ; sans cela, le fichier est enregistré sous download. Le type MIME par défaut pour une chaîne est text/plain.

appmintNative.saveToGallery appmintNative#

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

Exemple

Enregistre une photo ou une vidéo dans la galerie du téléphone - can('gallery') vaut 'native' dans l'application Android et 'web' dans un navigateur (le fichier y est téléchargé).

Renvoie : Promise de l'URI de l'élément enregistré ('' sur le web). Rejette avec code 'permission-denied', 'download-failed', 'bad-source' ou 'failed'. Nécessite : rien ; aucune autorisation sur 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>
  );
}

Enregistrez un clip vidéo et gérez les codes d'échec avec lesquels il peut rejeter :

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

Enregistrez une image depuis une URL de données, et ne la transmettez à la feuille de partage que si elle a été enregistrée :

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

Remarques : source peut aussi être une URL data: ou http(s) ; passez { kind: 'video' } pour une URL de vidéo sans type MIME vidéo.

appmintNative.scanBarcode appmintNative#

scanBarcode(video: HTMLVideoElement, opts?: { signal?, formats?, facingMode? }): Promise<{ value, format } | null>

Exemple

Ouvre la caméra dans un <video> que VOUS placez dans votre mise en page, et se résout avec le premier code-barres ou code QR qu'elle voit.

Retourne : Promise<{ value: string; format: string } | null> - le premier code, ou null en cas d'annulation (le signal a été interrompu), de caméra refusée ou quand l'analyse n'est pas disponible. Ne rejette jamais et arrête toujours la caméra. Nécessite : l'accès à la caméra - dans l'application Android, activez Caméra à l'étape 4 (Accès) lors de la compilation.

Utilisez-le seulement quand le viseur doit se trouver dans votre propre écran ; sinon scanCode() est plus simple.

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

Utiliser la caméra avant :

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

declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });

Fermez le scanner après la première lecture et proposez un code saisi quand la caméra n'est pas 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>
  );
}

Remarques :

  • Il appelle getUserMedia (l'utilisateur voit donc l'invite d'autorisation de la caméra) et décode avec BarcodeDetector à chaque image. can('barcodeScan') vaut 'none' lorsque l'un ou l'autre est absent.
  • La compilation analyse votre code : un appel scanBarcode( active Camera pour vous. Si vous l'avez dĂ©sactivĂ© Ă la main, la demande d'accĂšs Ă la camĂ©ra Ă©choue et vous obtenez null.
  • DĂ©marrez-le depuis une action de l'utilisateur (un bouton Scanner ou l'ouverture d'un Ă©cran de scan), jamais au lancement de l'application. Traitez null comme un rĂ©sultat normal avec une option de saisie manuelle.

appmintNative.scanCode appmintNative#

scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>

Exemple

Ouvre un scanner plein écran et résout avec le premier code-barres ou QR code - un seul appel, rien à dessiner.

Retourne : Promise<ScanCodeResult | null> - { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }, ou null quand l'utilisateur a fermé le scanner. Tout le reste rejette avec une Error dont code vaut 'unavailable' (pas de scanner ici - services Play manquants dans l'application ; pas de BarcodeDetector ou de caméra sur le web), 'busy' (un scan est déjà ouvert), 'camera-denied' (web : autorisation de la caméra refusée) ou 'failed'. Nécessite : rien dans l'application (le scanner de Google n'a pas besoin d'autorisation de caméra).

Depuis un bouton Scanner :

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

Utilisez les éléments analysés que l'application remplit (Wi-Fi, liens, contacts …), avec une saisie manuelle de secours dans le scanner 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);

Reportez depuis un bouton de votre propre écran d'alarme, en utilisant onAlarm pour savoir quand elle sonne.

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 répond false quand rien ne sonne, vérifiez donc avant de promettre à l'utilisateur une seconde sonnerie.

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

Remarques :

  • Dans l'application, il s'agit du scanner de codes de Google (services Play, via le AppMint.scanCode du shell) : zoom automatique, pas de demande d'accĂšs Ă la camĂ©ra. type vaut 'url', 'wifi', 'text', 'phone', 'email', 'sms', 'geo', 'contact', 'calendar', 'isbn', 'product' ou 'driver_license', et le champ correspondant est renseignĂ©. manualInput permet Ă l'utilisateur de saisir le code.
  • Sur le web, il dessine sa propre couche caméra plein écran (bouton de fermeture, le geste de retour la ferme) et décode avec BarcodeDetector ; seuls value et format y sont remplis, et le navigateur demande l'autorisation de la caméra.
  • null signifie toujours que l'utilisateur a fermĂ© le scanner ; tout autre rĂ©sultat rejette avec un code, affichez donc un message dans catch.
  • Appelez-la depuis un appui, jamais au chargement de la page. Restreignez formats quand vous les connaissez.

appmintNative.scheduleAlarm appmintNative#

scheduleAlarm(id, input) — @/lib/appmintNative

Exemple

Programme un vrai réveil depuis une application créée par IA : il sonne au volume d'alarme du téléphone (même en mode silencieux, et à travers Ne pas déranger quand l'utilisateur autorise les alarmes), continue de sonner jusqu'à Arrêter / Répéter ou ringSeconds, et place l'application à l'écran par-dessus l'écran de verrouillage.

Retourne : boolean, de façon synchrone - true si elle est programmée ; false sur le web, sur iOS, ou quand alarmSupport() n'est pas 'ready'. Le même id remplace une alarme plus ancienne. Nécessite : 🔔 Réveil à l'étape Accès (étape 4) lors de la compilation.

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

Proposer l'alarme seulement quand l'application peut en faire sonner une, et expliquer ce qui manque sinon :

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

Réagissez à la façon dont une alarme qui sonnait s'est terminée, et arrêtez-la depuis votre propre écran :

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

Remarques : Application Android uniquement. repeat quotidienne / hebdomadaire / jours ouvrés / mensuelle / annuelle gardent la même heure d'horloge malgré les changements d'heure d'été. Annulez avec cancelNotification(id) sur le pont ; les résultats arrivent via onAlarm.

appmintNative.ScreenOrientation appmintNative#

ScreenOrientation.lock({ orientation }) / ScreenOrientation.unlock()

Exemple

Verrouille l'écran en portrait ou en paysage pour une vue, sous la forme du plugin ScreenOrientation de Capacitor - le même code que lockOrientation().

Renvoie : Promise<void> pour les deux. lock rejette, comme celui de Capacitor, quand orientation n'est pas un type de verrouillage ou que la plateforme refuse le verrouillage. Nécessite : rien.

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

Verrouillez pour un seul écran seulement quand un verrouillage est autorisé, et laissez l'utilisateur faire pivoter en retour :

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

Quand vous avez besoin de savoir si le verrouillage a fonctionné, utilisez lockOrientation() directement (il répond false là où le verrouillage est refusé, comme les navigateurs de bureau) :

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

Remarques : orientation est l'un des types de verrouillage de Capacitor - 'any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'portrait-secondary', 'landscape-primary', 'landscape-secondary' - et est appliqué tel quel. Les navigateurs de bureau refusent les verrouillages ; les navigateurs mobiles ne les autorisent généralement qu'en plein écran. lockOrientation(to) retourne true/false. Donnez à l'utilisateur un moyen de sortir d'une vue verrouillée.

appmintNative.screenReader appmintNative#

screenReader.isEnabled() / screenReader.speak(text, lang?) / screenReader.onChange(cb)

Exemple

Indique à l'application si TalkBack est activé, et annonce de courts messages.

Retourne : isEnabled() → Promise<boolean> (false sur le web - il n'existe pas d'API web). speak() → Promise<void>. onChange(cb) → une fonction de désabonnement ; cb(enabled) s'exécute quand le lecteur d'écran est activé ou désactivé (dans l'application uniquement). Nécessite : rien.

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

N'annoncez un résultat qu'aux personnes qui utilisent le lecteur d'écran, et laissez les toasts visuels aux autres :

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

export async function notifySaved() {
  if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
  else toast('Note saved');
}

Retirez une animation pendant que TalkBack est activĂ©, et arrĂȘtez d'Ă©couter quand le composant disparaĂźt :

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

Remarques : Dans l'application, speak passe par le plugin ScreenReader ; sur le web (ou si le plugin échoue), il est lu par la synthèse vocale. Préférez un bon balisage (aria-label, régions aria-live) pour la plupart des annonces ; utilisez speak pour les changements courts et importants.

appmintNative.searchContacts appmintNative#

searchContacts(query: string, limit = 50): Promise<Contact[]>

Exemple

Trouve les contacts qui correspondent à un mot de recherche. Application uniquement.

Renvoie : Promise<Contact[]> - vide quand rien ne correspond, que l'accĂšs est refusĂ© ou que Contacts est dĂ©sactivĂ©. NĂ©cessite : la mĂȘme chose que listContacts : Contacts Ă l'Ă©tape 4 (Access) (activĂ© par la compilation quand votre code appelle ceci), et l'autorisation de l'utilisateur Ă la premiĂšre utilisation.

Champ de recherche avec un léger délai pendant la saisie.

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

Affichez un état clair quand Contacts n'est pas disponible et quand rien ne correspond :

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

Cherchez une personne par son nom, puis pré-remplissez un SMS pour elle :

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
}

Remarques : La correspondance est faite par la recherche de contacts du téléphone sur les noms. Chaque contact est { id?, name?, phones?: string[], emails?: string[], photoUri? }. Sur le web, can('contactList') vaut 'none' - il n'existe pas d'API web pour lire le carnet d'adresses. Les contacts sont des données personnelles : déclarez-les dans votre formulaire Sécurité des données de Play et dans votre politique de confidentialité.

appmintNative.SecureKeyError appmintNative#

class SecureKeyError extends Error { code } — from '@/lib/appmintNative'

Exemple

L'erreur que secureKeys lève. Son code est un mot court sur lequel vous pouvez brancher votre logique, identique dans l'application et sur le web.

Retourne : une Error avec name === 'SecureKeyError' et code : 'no-such-key', 'auth-required' (déverrouiller et réessayer), 'key-invalidated' (la biométrie ou le verrouillage d'écran a changé - les données sont perdues), 'strongbox-unavailable', 'disabled' (Clés sécurisées absentes de cette compilation / pas de Web Crypto), 'bad-alias', 'bad-payload' (base64 invalide ou trop court), 'too-large' (plus de 4 Mo en un appel sur Android), 'foreign-origin' (une page d'un autre site l'a demandé), 'failed' (tout autre cas). Nécessite : rien.

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

Transformez le code en un court message pour l'utilisateur en un seul endroit :

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

Traitez une charge altérée ou erronée comme un résultat normal et effacez-la, mais gardez une clé manquante comme une erreur à afficher :

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

Remarques : e.message vaut code ou "code: detail" ; faites vos tests sur e.code, jamais sur le message. Les termes d'erreur propres aux plateformes sont convertis en ces codes, et leur texte d'origine est conservé dans le message. Une charge modifiée ou étrangÚre donne 'bad-payload' sur le web et 'failed' dans l'application, qui ne peut pas la distinguer des autres erreurs de chiffrement.

appmintNative.secureKeys appmintNative#

secureKeys — generate / encrypt / decrypt / remove / has / list / isHardwareBacked

Exemple

Des clés de chiffrement que l'appareil conserve et que l'application ne peut jamais exporter : le Keystore d'Android dans l'application, et une clé Web Crypto non extractible sur le web. Supprimer la clé est la seule vraie « suppression sécurisée ».

Retourne : toutes des Promises. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked} ; encrypt(alias, text) → chaîne base64 ; decrypt(alias, base64) → le texte ; remove(alias) / has(alias) / isHardwareBacked(alias) → booléen ; list() → string[]. generate, encrypt et decrypt lèvent SecureKeyError avec un code. Nécessite : Clés sécurisées (chiffrement) à l'étape 4 (Accès) lors de la compilation.

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

Supprimez pour de bon - personne ne peut lire les données scellées aprÚs cela :

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

export async function wipeDiary() {
  await secureKeys.remove('diary');
  localStorage.removeItem('diary');
}

Listez les clés que détient l'application et indiquez si chacune est protégée par le matériel (le web répond toujours 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'}`));
}

Remarques : can('secureKeys') vaut 'native' dans l'application et 'web' dans un navigateur, oĂč isHardwareBacked() est toujours false - ne dites jamais « matĂ©riel » dans ce cas. Une clĂ© requireAuth : dans l'application, encrypt/decrypt lĂšvent auth-required sans rien afficher une fois la fenĂȘtre de dĂ©verrouillage Ă©coulĂ©e - appelez unlock() et rĂ©essayez ; sur le web, le runtime affiche unlock() lui-mĂȘme. Chiffrez un enregistrement Ă la fois (l'application limite un appel Ă 4 Mo). Alias : lettres, chiffres, _ - ., 64 au maximum.

appmintNative.sendSms appmintNative#

sendSms(phoneNumber: string, message: string): Promise<boolean>

Exemple

Envoie un SMS directement, sans écran de confirmation. Application Android uniquement.

Retourne : Promise<boolean> - true quand le téléphone signale le message comme envoyé, false en cas d'échec, quand les SMS n'étaient pas activés ou quand l'utilisateur a refusé l'autorisation. Nécessite : l'interrupteur SMS (lecture et envoi) à l'étape 4 (Accès) - la compilation l'active quand votre code l'appelle - et l'autorisation de l'utilisateur à la première utilisation.

Envoyez une alerte et dites à l'utilisateur ce qui s'est passé.

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

Demandez d'abord une confirmation, car le message part sans aucun écran pour l'annuler :

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

Envoyez la mĂȘme alerte Ă plusieurs personnes, une par une, et comptez ce qui est parti :

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

Remarques : L'enrobage se résout à la PREMIÈRE réponse, qui est sent (le message a quitté le téléphone), pas delivered. Pour les accusés de réception, utilisez le WebToApk.sendSms brut avec l'événement appmint:sms. can('smsSend') vaut 'none' sur le web (aucun navigateur ne peut envoyer de SMS). Chaque SMS peut coûter de l'argent à l'utilisateur. Le SMS est une autorisation restreinte de Google Play : il vous faut une déclaration d'autorisations approuvée dans la Play Console, préférez donc composeSms() quand l'utilisateur peut appuyer sur envoyer.

appmintNative.setInAppPresentation appmintNative#

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

Exemple

Choisit qui dessine un message intégré (les campagnes du créateur) : l'application ('auto', par défaut : le design bannière, carte, feuille ou plein écran du créateur) ou votre propre interface ('none', via onInAppMessage).

Retourne : boolean de façon synchrone : true quand c'est enregistré (cela reste sur le téléphone). Toujours false sur le web et sans AppMint Push. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

// The app draws its own messages from now on.
setInAppPresentation('none');

Un interrupteur de paramĂštres :

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

export function onNativePopupsChange(on: boolean) {
  if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}

Prenez la main pendant qu'un écran est ouvert, puis rendez-la :

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

export function useQuietScreen() {
  useEffect(() => {
    setInAppPresentation('none');
    return () => { setInAppPresentation('auto'); };
  }, []);
}

Remarques : un message remis à votre code compte comme affiché. Définir 'none' ferme aussi celui qui est à l'écran en ce moment.

appmintNative.setMusicVolume appmintNative#

setMusicVolume(percent) — @/lib/appmintNative

Exemple

Règle le niveau de la musique d'ambiance en boucle de l'application, de 0 à 100 (arrondi et borné pour vous).

Retourne : void. Nécessite : un fichier choisi sous Musique d'ambiance à l'étape 3 (Intégrer) lors de la compilation. Ne fait rien quand musicVolume() vaut -1.

L'application conserve le niveau pour le prochain lancement, donc un simple bouton de coupure du son suffit :

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

export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40);   // remembered by the app

Un curseur de volume qui n'apparaît que quand l'application embarque de la musique :

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

Baissez la musique pendant la lecture d'une vidéo et remettez-la aprÚ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; }

Remarques : 0 rend la musique silencieuse mais elle continue de jouer. Le niveau est enregistré sur le téléphone (jusqu'à ce que l'utilisateur efface les données de l'application). Application Android uniquement ; sur le web, rien ne se passe.

appmintNative.setPushPresentation appmintNative#

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

Exemple

Choisit qui affiche un message AppMint Push envoyé avec un style intégré à l'application (bannière, fenêtre contextuelle, feuille du bas ou plein écran) pendant que l'application est ouverte : la coque de l'application ('auto', par défaut) ou votre propre code ('none').

Retourne : boolean de façon synchrone - true quand le mode a été enregistré. Toujours false sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation.

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

Vérifiez la réponse, car elle vaut false sur le web ou quand AppMint Push est désactivé :

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

Dessinez votre propre bannière pour chaque message et gardez la boîte de réception synchronisée :

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

Remarques : la boĂźte de rĂ©ception et l'Ă©vĂ©nement appmint:push fonctionnent de la mĂȘme façon dans les deux modes. DĂ©finir 'none' ferme aussi un message que le shell affiche en ce moment.

appmintNative.setPushTag appmintNative#

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

Exemple

Définit un tag AppMint Push sur cette installation. Le centre d'envoi peut envoyer à tous ceux qui ont un tag, et une automatisation de tag peut envoyer un message à un moment donné aprÚs la définition du tag. '' supprime le tag.

Retourne : boolean de façon synchrone - true quand c'est enregistré ; false quand la clé n'est pas composée de 1 à 32 lettres, chiffres ou _, que la valeur dépasse 128 caractères ou que l'installation dépasserait 50 tags. Toujours false sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation ; le ciblage par tags et les automatisations atteignent les applications compilées à partir du 30 sept. 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', '');
}

VĂ©rifiez le rĂ©sultat : false signifie que le tag a Ă©tĂ© refusĂ© (clĂ© invalide, valeur de plus de 128 caractĂšres, plus de 50 tags) ou que vous ĂȘtes sur le web ; conservez donc votre propre suivi de l'Ă©tat.

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

Étiqueter un téléphone une fois qu'il atteint une étape, pour que le centre d'envoi puisse cibler tous ceux qui ont ce niveau.

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

Remarques : les tags appartiennent à l'installation, pas à l'utilisateur connecté. Ne mettez jamais de données privées dans un tag.

appmintNative.setPushTags appmintNative#

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

Exemple

Définit plusieurs tags AppMint Push en une seule modification ; une valeur '' supprime ce tag, les tags que vous ne nommez pas sont conservés.

Renvoie : boolean de façon synchrone - true quand c'est enregistrĂ© ; false quand une clĂ© ou une valeur enfreint les rĂšgles de setPushTag (rien n'est alors enregistrĂ©). Toujours false sur le web. NĂ©cessite : Android : AppMint Push activĂ© Ă l'Ă©tape Integrate (Ă©tape 3) lors de la compilation ; le ciblage par tags et les automatisations touchent les applications compilĂ©es Ă partir du 30 sept. 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('');
}

Étiqueter un téléphone avec la langue et les sujets choisis quand l'utilisateur enregistre ses paramètres, et vérifier que chaque tag a été accepté.

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

Gardez les tags synchronisés avec le compte connecté dans un effet React, en supprimant ceux qui ne s'appliquent plus.

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

Remarques : tout ce qui est contenu dans un appel est enregistré, ou rien.

appmintNative.setPushUser appmintNative#

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

Exemple

Indique à AppMint Push lequel de vos utilisateurs est connecté sur cette installation, pour que le centre d'envoi puisse envoyer un message à cet utilisateur précis. Transmettez votre propre identifiant de compte stable après la connexion et '' à la déconnexion.

Retourne : boolean de façon synchrone - true quand l'identifiant a été enregistré ; false pour un identifiant contenant autre chose que des lettres, des chiffres et _ @ . : + - (jusqu'à 128). Toujours false sur le web. Nécessite : Android : AppMint Push activé à l'étape Intégrer (étape 3) lors de la compilation ; l'envoi à un utilisateur atteint les applications compilées à partir du 30 sept. 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();
  }, []);
}

Gérez un identifiant refusé : setPushUser répond false pour une adresse e-mail ou tout identifiant contenant des caractÚres hors de l'ensemble autorisé, envoyez donc plutÎt un identifiant stable.

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

Effacez l'identifiant quand l'utilisateur se déconnecte, dans un simple gestionnaire de clic.

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

export function SignOutButton({ signOut }: { signOut: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await signOut();
        if (getPushUser()) setPushUser('');   // this install stops receiving "send to this user" messages
      }}
    >
      Sign out
    </button>
  );
}

Combinez-la avec un tag pour qu'une seule étape de connexion identifie l'utilisateur et son offre ensemble.

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

export function onSignedIn(userId: string, plan: 'free' | 'pro') {
  setPushUser(userId);
  setPushTag('plan', plan);
}

Remarques : l'identifiant appartient à l'installation, la déconnexion doit donc l'effacer. Cela n'efface pas les tags - appelez setPushTags avec des valeurs '' pour ceux propres à l'utilisateur.

appmintNative.setStatusBar appmintNative#

setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void

Exemple

Colore la barre d'état du systÚme pour qu'elle corresponde à l'écran, et choisit des icÎnes claires ou sombres pour elle.

Retourne : rien. Nécessite : rien.

style est la couleur des ICÎNES : 'light' = icĂŽnes claires pour une barre sombre (par dĂ©faut), 'dark' = icĂŽnes sombres pour une barre claire. Appelez-la une fois au dĂ©marrage avec le --bg de l'application, et de nouveau sur tout Ă©cran ayant un autre arriĂšre-plan :

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

Un écran clair :

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

setStatusBar('#FFFFFF', 'dark');

La forme à la Capacitor (le style porte le nom du FOND : 'DARK' = icônes claires) :

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

await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });

Remarques :

  • Dans l'application Android, le shell peint lui-mĂȘme la barre, ce qui fonctionne aussi sur Android 15 en bord Ă bord ; une couleur illisible est ignorĂ©e - passez #RRGGBB. Un appel avec le style seul (backgroundColor = null) garde la couleur que cette page a dĂ©finie en dernier et inverse les icĂŽnes ; avant que la page ait dĂ©fini une couleur, il inverse les icĂŽnes via le plugin StatusBar et la barre garde la couleur choisie Ă la compilation.
  • Sur le web, elle dĂ©finit <meta name="theme-color"> ; le navigateur choisit lui-mĂȘme la couleur des icĂŽnes.

appmintNative.setVolume appmintNative#

setVolume(stream, percent, showUi?) — @/lib/appmintNative

Exemple

Définit le niveau de volume du téléphone pour un flux, de 0 à 100. showUi affiche le curseur de volume du téléphone pendant qu'il bouge.

Retourne : { ok, stream?, current?, max?, percent?, error? } - l'erreur est unknown_stream, bad_percent, fixed_volume, dnd_access_required (Ne pas déranger bloque la sonnerie / les notifications - appelez requestDndAccess()), muted (le mode « Silence total » de Ne pas déranger coupe le flux, le niveau n'a donc pas changé), not_applied, foreign_origin, failed, ou unavailable sur le web. Nécessite : rien pour le multimédia, l'alarme et l'appel.

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

Affichez le volume multimédia sous forme de curseur qui suit les touches matérielles, avec 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))} />;
}

Monter le flux d'alarme avant qu'un écran d'alarme ne sonne, et signaler pourquoi il n'a pas changé.

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>

Exemple

Ouvre la feuille de partage du système dans l'application, et l'API Web Share dans un navigateur.

Retourne : Promise<boolean> - true quand la feuille s'est ouverte ; false quand l'utilisateur l'a fermée ou que le partage n'est pas disponible. Rejette quand le partage lui-même est invalide (par exemple une url non prise en charge) ou qu'un autre partage est encore ouvert. Nécessite : rien.

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

Du texte seul (sans lien) convient aussi :

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

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

La forme Capacitor Share.share(opts) appelle le mĂȘme code mais REJETTE quand l'utilisateur annule (contrat de Capacitor) et rĂ©sout { activityType: '' } sinon ; Share.canShare() rĂ©sout { 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 */
  }
}

Remarques :

  • Ne transmettez que les champs dont vous disposez. Dans l'application, le texte et les liens passent par le plugin Capacitor Share.
  • files fonctionne dans l'application Android (les fichiers arrivent dans la feuille de partage comme de vraies pièces jointes ; un url est ajouté au texte) et sur le web là où le navigateur peut partager des fichiers (navigator.canShare({ files })). Gardez les fichiers petits - ils sont transmis à l'application en mémoire.
  • Sur le web, appelez-la depuis un appui ; les navigateurs refusent un partage qu'aucune action de l'utilisateur n'a lancé.

appmintNative.Share appmintNative#

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

Exemple

Documenté avec appmintNative.share : l'exemple qui s'y trouve illustre aussi celui-ci.

Ouvre la feuille de partage du système dans l'application, et l'API Web Share dans un navigateur.

Retourne : Promise<boolean> - true quand la feuille s'est ouverte ; false quand l'utilisateur l'a fermée ou que le partage n'est pas disponible. Rejette quand le partage lui-même est invalide (par exemple une url non prise en charge) ou qu'un autre partage est encore ouvert. Nécessite : rien.

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

Du texte seul (sans lien) convient aussi :

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

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

La forme Capacitor Share.share(opts) appelle le mĂȘme code mais REJETTE quand l'utilisateur annule (contrat de Capacitor) et rĂ©sout { activityType: '' } sinon ; Share.canShare() rĂ©sout { 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 */
  }
}

Remarques :

  • Ne transmettez que les champs dont vous disposez. Dans l'application, le texte et les liens passent par le plugin Capacitor Share.
  • files fonctionne dans l'application Android (les fichiers arrivent dans la feuille de partage comme de vraies pièces jointes ; un url est ajouté au texte) et sur le web là où le navigateur peut partager des fichiers (navigator.canShare({ files })). Gardez les fichiers petits - ils sont transmis à l'application en mémoire.
  • Sur le web, appelez-la depuis un appui ; les navigateurs refusent un partage qu'aucune action de l'utilisateur n'a lancé.

appmintNative.sharedText appmintNative#

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

Exemple

Fournit le texte ou le lien qu'une autre application a partagé VERS cette application (« Partager → votre application »), par exemple un lien partagé depuis un navigateur ou YouTube.

Renvoie : de façon synchrone, { text, subject, url, receivedAt } ou null quand rien n'attend. url est le premier lien http(s) trouvĂ© dans text ('' s'il n'y en a pas) ; receivedAt est en millisecondes depuis 1970. NĂ©cessite : l'option Receive Shares From Other Apps de la compilation avec « Text and links » - activĂ©e automatiquement quand votre code appelle sharedText( ou onShared(.

Vérifiez au montage, enregistrez, puis effacez :

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

Écoutez un partage qui arrive alors que l'application est déjà ouverte avec onShared, et effacez-le après usage.

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

PrĂ©remplissez un formulaire Ă partir d'un lien partagĂ© et n'affichez rien sur le web, oĂč sharedText() vaut 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" />;
}

Remarques :

  • Le partage est CONSERVÉ jusqu'à ce que vous appeliez clearShared(), donc une application qui se monte après le partage le voit encore. Effacez-le après l'enregistrement, sinon le montage suivant l'enregistre à nouveau.
  • Pour un partage qui arrive pendant que l'application est ouverte, utilisez onShared().
  • Une photo ou une vidéo partagée AVEC une légende : la légende arrive ici, le fichier par l'API de fichier ouvert.
  • Application Android uniquement. can('shareTarget') vaut 'none' sur le web, et sharedText() y retourne null.

appmintNative.sheet appmintNative#

sheet(opts: { title?, content, dismissible? }): { close(), element, closed }

Exemple

Ouvre une feuille modale du bas avec un vrai comportement de feuille - pour un panneau de filtres, un éditeur ou un aperçu détaillé.

Retourne : un handle : close() la ferme, element est l'élément du corps (remplissez-le ou mettez-le à jour), closed est une Promise<void> qui se résout quand la feuille est fermée, quelle que soit la manière. Nécessite : rien.

Options : title (titre facultatif), content - un élément, du texte brut (pas du HTML) ou une fonction (host) => { … } qui remplit le corps ; dismissible (true par défaut : un appui sur le fond, un glissement vers le bas, Échap et le geste de retour la ferment).

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

Contenu React, rendu dans la feuille avec un portail :

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

Une feuille qui reste ouverte jusqu'à ce que l'utilisateur choisisse, avec dismissible: false et la promesse 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;
}

Remarques :

  • Dessinée avec les jetons de l'application (--surface, --ink, --muted, --border), au-dessus de la zone sûre, avec un léger retour haptique à l'ouverture.
  • Glisser vers le bas la ferme quand on dépasse 120 px ou en un geste rapide ; le corps défile d'abord quand il n'est pas tout en haut.
  • Avec dismissible: false, seul votre propre close() la ferme - donnez un bouton Ă l'utilisateur.
  • Pour une simple liste d'actions, utilisez actionSheet().

appmintNative.shortcuts appmintNative#

shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()

Exemple

Ajoute des raccourcis au menu d'appui long de l'icône de l'application (raccourcis dynamiques Android). Un appui ouvre l'application sur cette route.

Renvoie : set() renvoie Promise<boolean> - true quand les raccourcis ont Ă©tĂ© remis au systĂšme, false lĂ oĂč il n'y en a pas (le web). clear() renvoie Promise<void>. NĂ©cessite : rien.

Définissez-les une fois au démarrage, et aiguillez selon le lien de lancement :

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

Supprimez-les (par exemple après la déconnexion) :

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

await shortcuts.clear();

Ouvrez l'écran correspondant quand un raccourci est touché alors que l'application est déjà en cours d'exécution, avec 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]);
}

Afficher des raccourcis seulement pour un utilisateur connecté, et gérer le web, où set() répond 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]);
}

Remarques :

  • Au maximum 4 Ă©lĂ©ments ; les Ă©lĂ©ments en trop sont ignorĂ©s. Un route sans / initial en reçoit un. id est facultatif.
  • Android 7.1+ ; sur un Android plus ancien, l'appel ne fait rien. Les titres sont coupés à 25 caractères (libellé court) et 50 (libellé long).
  • Un appui ouvre l'application avec le lien de lancement appmint-shortcut://<package><route>, donc new URL(u).pathname est votre route. Lisez-le avec launchUrl() au montage et onDeepLink() pendant que l'application s'exécute.
  • can('shortcuts') vaut 'none' sur le web : un navigateur n'a pas d'icĂŽne d'application.

appmintNative.snoozeAlarm appmintNative#

snoozeAlarm(id?, minutes?) — @/lib/appmintNative

Exemple

ArrĂȘte l'alarme qui sonne et la fait sonner de nouveau aprĂšs minutes (0 ou omis = le snoozeMinutes propre Ă l'alarme, 10 par dĂ©faut). La sonnerie de demain d'une alarme rĂ©pĂ©tĂ©e ne bouge pas.

Retourne : boolean - true quand elle sonnait et est reportée. Nécessite : 🔔 Réveil à l'étape Accès (étape 4) lors de la compilation.

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

Reportez depuis un bouton de votre propre écran d'alarme, en utilisant onAlarm pour savoir quand elle sonne.

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 répond false quand rien ne sonne, vérifiez donc avant de promettre à l'utilisateur une seconde sonnerie.

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

Exemple

Lit du texte à voix haute avec le moteur de synthÚse vocale du téléphone.

Renvoie : void - elle commence à parler (ou se met en file aprÚs la lecture en cours) et n'envoie aucun signal de fin. opts : { lang?: string; rate?: number; pitch?: number } (par défaut : langue du moteur, 1, 1). Nécessite : rien.

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

Différentes langues et vitesses :

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

Dites une courte confirmation aprÚs une action, et informez aussi l'utilisateur par écrit.

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

export function announceSaved(title: string) {
  toast('Saved');
  speak(`${title} saved`, { rate: 1.1 });
}

Lisez une liste élément après élément ; speak() met en file d'attente, appelez-le donc dans une boucle et arrêtez tout avec stopSpeaking() quand l'écran se ferme.

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

Remarques : Dans l'application : le moteur natif de l'application. Sur le web : speechSynthesis. Plusieurs appels speak() se jouent l'un après l'autre. Si vous devez savoir quand la parole se termine (pour surligner des mots ou passer à la suite), utilisez window.speechSynthesis avec le onend d'un énoncé directement ; cela fonctionne aussi dans l'application Android.

appmintNative.speech appmintNative#

speech — @/lib/appmintNative

Exemple

Dictée : transforme la voix de l'utilisateur en texte. Le moteur de reconnaissance vocale d'Android dans l'application Android, la Web Speech API dans Chrome.

Retourne : speech.isAvailable() → Promise<boolean>. speech.start(onResult, opts?) → Promise<boolean> : true quand l'écoute a démarré, false quand l'autorisation du microphone a été refusée ou que la dictée n'est pas disponible. onResult(text, isFinal) est appelé avec le texte. speech.stop() → Promise<void>. opts : { lang?: string; partial?: boolean; prompt?: string }. Nécessite : Micro à l'étape 4 (Accès) ; les compilations IA l'activent automatiquement quand le code utilise la reconnaissance vocale.

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

export function VoiceNote() {
  const [ok, setOk] = useState(false);
  const [text, setText] = useState('');
  const [msg, setMsg] = useState('');

  useEffect(() => { void speech.isAvailable().then(setOk); }, []);
  if (!ok) return null;                      // no dictation here: hide the mic button

  const listen = async () => {
    setMsg('Listening...');
    const started = await speech.start(
      (t, isFinal) => { setText(t); if (isFinal) setMsg(''); },
      { lang: 'en-US' },                     // partial results by default
    );
    if (!started) setMsg('Microphone permission is needed for voice input.');
  };

  return (
    <div>
      <button aria-label="Speak" onClick={listen}>🎤</button>
      <button onClick={() => { void speech.stop(); setMsg(''); }}>Stop</button>
      <p>{text}</p>
      <p role="status">{msg}</p>
    </div>
  );
}

Dictez dans un champ de texte et dites-le quand le microphone a été refusé, puisque start() répond 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>
  );
}

Arrêtez l'écoute quand l'écran se ferme, et relisez le résultat avec 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>
  );
}

Remarques :

  • Avec partial activé (par défaut), onResult(text, false) arrive au fil des mots, puis une fois onResult(text, true) avec le texte final. Avec partial: false, vous n'obtenez que le final, quand l'utilisateur cesse de parler.
  • L'invite d'autorisation s'affiche au premier start().
  • Si l'utilisateur ne dit rien, onResult n'est pas appelé ; l'écoute se termine simplement. Gardez un bouton Arrêter ou un délai d'expiration dans votre interface.
  • L'écoute s'arrête d'elle-même après une pause. lang prend par défaut la langue du téléphone.

appmintNative.splash appmintNative#

splash.hold() / splash.ready()

Exemple

Garde l'écran de démarrage affiché jusqu'à ce que le premier vrai écran soit prêt, au lieu d'afficher « écran de démarrage, puis un indicateur de chargement ».

Renvoie : rien (les deux sont synchrones). Nécessite : rien.

Par défaut, l'écran de démarrage se masque tout seul au premier affichage de l'application - vous n'avez rien à faire. Quand le premier écran a besoin de données, retenez l'écran de démarrage en haut du module et libérez-le quand cet écran est affiché :

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

Retenez l'écran de démarrage pendant la restauration d'une session connectée, et libérez-le dans un finally pour qu'un échec ne le garde pas affiché.

// main.tsx
import { splash } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';

splash.hold();
supabase.auth.getSession().finally(() => splash.ready());

Libérez-le une fois, depuis un hook, après l'affichage des données du premier écran.

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

Remarques :

  • Dans l'application Android, hold() demande au shell de garder son Ă©cran de dĂ©marrage ; ready() le masque (la durĂ©e minimale d'Ă©cran de dĂ©marrage dĂ©finie par le crĂ©ateur s'applique toujours).
  • Sur le web, les deux ne font rien - il n'y a pas d'Ă©cran de dĂ©marrage.
  • La coque limite l'attente à environ 6 secondes : une page qui n'appelle jamais ready() s'ouvre donc quand même.

appmintNative.StatusBar appmintNative#

StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })

Exemple

La barre d'état sous la forme du plugin StatusBar de Capacitor, construite sur setStatusBar().

Retourne : Promise<void> pour les deux. Nécessite : rien.

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

Couleur et icÎnes ensemble - préférez setStatusBar(), qui définit les deux en un seul appel :

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

setStatusBar('#0B0F1A', 'light');   // dark bar, light icons
setStatusBar('#FFFFFF', 'dark');    // light bar, dark icons

Faire basculer la barre avec le thème clair et sombre propre à l'application, en combinant les appels à la façon de 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]);
}

Remarques : style suit Capacitor : 'DARK' signifie un fond sombre (icônes claires), toute autre valeur signifie des icônes sombres. Dans l'application Android, le pont de l'enveloppe peint la barre (l'appel de couleur du plugin ne fait rien sur Android 15+), et setStyle() inverse les icônes selon la couleur actuelle de la barre. Sur le web, la couleur devient <meta name="theme-color">.

appmintNative.stopAlarm appmintNative#

stopAlarm(id?) — @/lib/appmintNative

Exemple

ArrĂȘte l'alarme qui sonne, comme son bouton Ignorer. Une alarme rĂ©pĂ©tĂ©e reste programmĂ©e pour sa prochaine occurrence.

Retourne : boolean - true quand elle sonnait et est maintenant arrêtée. id omis = celle qui sonne. Nécessite : 🔔 Réveil à l'étape Accès (étape 4) lors de la compilation.

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

ArrĂȘtez l'alarme depuis l'Ă©cran que l'application ouvre quand l'alarme sonne, et rĂ©agissez Ă chaque façon dont elle se termine :

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

Vérifiez la réponse : false signifie que rien ne sonnait (ou que vous êtes sur le web), ne prétendez donc pas que c'est arrêté :

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

Exemple

Arrête la synthèse vocale immédiatement et supprime tout ce qui attend d'être lu.

Renvoie : void. Nécessite : rien.

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

Lisez un long texte Ă voix haute avec la langue et la vitesse choisies par l'utilisateur, et arrĂȘtez-le depuis un seul bouton :

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

Coupez la voix quand l'utilisateur commence à taper une réponse :

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

export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
  return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}

Remarques : Sans risque à appeler quand rien n'est lu. Dans l'application Android : l'arrêt du moteur natif. Sur le web : speechSynthesis.cancel().

appmintNative.swipeable appmintNative#

swipeable(el: HTMLElement, opts: SwipeOptions): () => void

Exemple

Ajoute des gestes de balayage à un élément - lignes à supprimer par balayage, pagination, fermeture d'une carte.

Retourne : une fonction de désabonnement qui restaure aussi le touch-action d'origine de l'élément. Nécessite : rien.

Options : onLeft, onRight, onUp, onDown (au moins une), threshold (px de déplacement, 56 par défaut), edgeGuard (px depuis le bord gauche/droit de l'écran où les balayages horizontaux sont ignorés, 24 par défaut), haptics (retour haptique de sélection lors d'un balayage, par défaut true).

Balayer vers la gauche pour supprimer, avec un bouton visible pour la même action :

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

Passez d'un onglet Ă l'autre avec gauche / droite :

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

Fermez une carte en la balayant vers le haut, le gestionnaire étant retiré au démontage :

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

Remarques :

  • Construit sur les Pointer Events : le même code fonctionne donc au toucher, au stylet et au glisser de souris.
  • Il définit touch-action pour vous : pan-y pour les gestionnaires horizontaux (le défilement vertical fonctionne toujours), pan-x pour les verticaux, none pour les deux.
  • Un balayage doit être nettement dans une direction (1,5 fois plus sur un axe) ; les défilements en diagonale ne se déclenchent pas. Un second doigt annule le geste.
  • La garde de bord existe parce que le geste de retour système d'Android occupe les bords de l'écran.

appmintNative.systemPalette appmintNative#

systemPalette(): Promise<SystemPalette>

Exemple

Lit les couleurs Material You du téléphone (construites à partir du fond d'écran sur Android 12+), pour que la couleur d'accent de l'application puisse suivre le fond d'écran de l'utilisateur comme les applications système.

Renvoie : Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - chaque nuance associe un ton ('0', '10', '50', '100' … '900', '1000') Ă une couleur hexadĂ©cimale. { available: false } sous Android 12 et sur le web. NĂ©cessite : rien.

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

N'utilisez les couleurs du fond d'écran que lorsque le téléphone les propose, et relisez-les quand l'application revient à l'écran :

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

Affichez l'option « Adapter à mon fond d'écran » uniquement quand une palette existe, en utilisant { available: false } comme réponse à « pas ici » :

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

Remarques : VĂ©rifiez le contraste avant d'utiliser une teinte du fond d'Ă©cran pour du texte ou des boutons - --accent doit toujours atteindre 3:1 sur votre arriĂšre-plan. Proposez-le comme option (« Assortir Ă mon fond d'Ă©cran »), pas comme thĂšme imposĂ©. Appelez-la de nouveau Ă la reprise de l'application ; l'utilisateur a pu changer de fond d'Ă©cran.

appmintNative.TextToSpeech appmintNative#

TextToSpeech — @/lib/appmintNative

Exemple

L'objet TextToSpeech à la Capacitor (speak / stop), qui s'exécute sur le même code que speak() et stopSpeaking().

Renvoie : speak({ text, lang?, rate?, pitch? }) → Promise<void> qui se rĂ©sout une fois le texte prononcĂ© (ou que stop() l'a interrompu) et rejette quand il n'a pas pu ĂȘtre prononcĂ©. stop() → Promise<void>. NĂ©cessite : rien.

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

Lisez à voix haute une liste de lignes l'une après l'autre ; chaque speak se résout une fois mis en file d'attente, le moteur les joue donc dans l'ordre :

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 bouton de lecture à voix haute qui arrête la lecture quand on quitte l'écran :

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

Remarques : await TextToSpeech.speak(…) est le signal de fin : l'étape suivante après la lecture peut le suivre directement. Dans l'application : le moteur de synthèse vocale natif d'Android de l'enveloppe ; sur le web : speechSynthesis.

appmintNative.textZoom appmintNative#

textZoom.get() / textZoom.getPreferred() / textZoom.set(value)

Exemple

Un contrÎle de taille du texte intégré. Dans l'application, il modifie le zoom du texte de la page (1 = 100 %) ; sur le web, il modifie la taille de police racine, ce qui redimensionne toute mise en page basée sur rem.

Retourne : get() → Promise<number>. getPreferred() → Promise<number> - la taille choisie par l'utilisateur dans les paramètres du téléphone (1 sur le web). set(value) → Promise<void> ; la valeur est limitée à 0,5-3, et la promesse est rejetée si la valeur n'est pas un nombre. Nécessite : rien.

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

Mémorisez le choix entre les lancements :

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 sélecteur de taille de texte qui lit la valeur actuelle, la garde dans la plage autorisée et l'affiche en pourcentage :

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

Remarques : L'application suit déjà le réglage de taille de police du téléphone ; ceci est un contrôle supplémentaire, dans l'application. Testez la mise en page avec de grandes tailles - les hauteurs fixes sur les champs de texte coupent le contenu.

appmintNative.toast appmintNative#

toast(message: string, opts?: { duration?: 'short' | 'long' }): void

Exemple

Affiche un bref message qui n'appelle aucune réponse - « Copié », « Enregistré », « Ajouté à la liste ».

Renvoie : rien (on lance et on oublie). Nécessite : rien.

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 message plus long reste affiché environ 3,5 secondes :

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

toast('Your export will be ready in a minute', { duration: 'long' });

La forme à la Capacitor Toast.show({ text, duration? }) fait la même chose et se résout en Promise<void>:

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

await Toast.show({ text: 'Added to list' });

Pour un message que l'utilisateur DOIT voir, utilisez plutôt une boîte de dialogue :

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

Remarques :

  • Dans l'application Android, le plugin Capacitor Toast l'affiche en bas de l'écran.
  • Partout ailleurs, c'est une petite snackbar dessinée sur la page aux couleurs de l'application (--ink sur --bg), au-dessus de l'encart de zone sûre, avec role="status" pour que les lecteurs d'écran l'annoncent.
  • Un seul est affiché à la fois ; un nouveau toast remplace l'ancien.
  • Un toast est volontairement facile à manquer. Ne l'utilisez jamais pour une erreur sur laquelle l'utilisateur doit agir.

appmintNative.Toast appmintNative#

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

Exemple

Documenté avec appmintNative.toast : l'exemple qui s'y trouve illustre aussi celui-ci.

Affiche un bref message qui n'appelle aucune réponse - « Copié », « Enregistré », « Ajouté à la liste ».

Renvoie : rien (on lance et on oublie). Nécessite : rien.

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 message plus long reste affiché environ 3,5 secondes :

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

toast('Your export will be ready in a minute', { duration: 'long' });

La forme à la Capacitor Toast.show({ text, duration? }) fait la même chose et se résout en Promise<void>:

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

await Toast.show({ text: 'Added to list' });

Pour un message que l'utilisateur DOIT voir, utilisez plutôt une boîte de dialogue :

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

Remarques :

  • Dans l'application Android, le plugin Capacitor Toast l'affiche en bas de l'écran.
  • Partout ailleurs, c'est une petite snackbar dessinée sur la page aux couleurs de l'application (--ink sur --bg), au-dessus de l'encart de zone sûre, avec role="status" pour que les lecteurs d'écran l'annoncent.
  • Un seul est affiché à la fois ; un nouveau toast remplace l'ancien.
  • Un toast est volontairement facile à manquer. Ne l'utilisez jamais pour une erreur sur laquelle l'utilisateur doit agir.

appmintNative.torch appmintNative#

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

Exemple

La lampe torche du téléphone : torch.on(), off(), toggle(), isOn(), isAvailable(). can('torch') vaut 'none' sur le web.

Retourne : on()/off() → Promise (rejetĂ©e avec code 'unavailable' ou 'CAMERA_IN_USE') ; toggle() → le nouvel Ă©tat ; isOn() / isAvailable() → boolĂ©en. NĂ©cessite : rien, aucune autorisation.

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

Éteignez la lumière quand l'utilisateur quitte l'écran et affichez l'erreur que signale le téléphone quand elle ne peut pas être basculée :

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

Faites clignoter la lumiÚre comme signal, puis laissez-la éteinte :

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

Remarques : désactivé quand l'application se ferme. Tant que le flux de caméra propre à l'application est ouvert, allumez plutôt la lampe avec la contrainte torch de la piste.

appmintNative.trackEvent appmintNative#

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

Exemple

Signale un moment dans votre application pour que les messages intégrés du créateur ayant le déclencheur « Quand un événement se produit » (même nom) s'affichent. L'application télécharge les campagnes du créateur depuis le centre d'envoi AppMint Push et les affiche elle-même.

Renvoie : boolean de façon synchrone : false pour un nom invalide (lettres, chiffres et _ . : -, jusqu'Ă 64), sur le web, et dans les applications compilĂ©es sans AppMint Push. NĂ©cessite : Android : AppMint Push activĂ© Ă l'Ă©tape Integrate (Ă©tape 3) lors de la compilation.

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
}

Signalez un changement de vue que l'URL n'affiche pas, pour que les messages « Sur une page » le voient :

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

export function selectTab(tab: 'home' | 'cart' | 'profile') {
  setActiveTab(tab);
  trackEvent('appmint:route', { path: '/' + tab });
}

Un hook qui suit la premiÚre ouverture d'un écran :

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

Remarques : les changements d'URL (chargements de page, pushState, routes #hash) sont déjà détectés ; ne signalez que ce que l'adresse ne change pas.

appmintNative.transition appmintNative#

transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>

Exemple

Exécute un changement d'écran avec une animation à l'aspect natif : push fait glisser le nouvel écran depuis la droite, pop le fait glisser en retour, fade fait un fondu enchaîné.

Retourne : Promise<void> qui se résout après le changement et son animation. update est votre changement d'état (un navigate de routeur, un setState) et il est toujours appliqué. Nécessite : rien.

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

Fondu entre deux onglets :

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

declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });

Utiliser un fondu pour un changement d'onglet, avec un gestionnaire par onglet :

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

Remarques :

  • Elle utilise la View Transitions API quand la WebView l'a. Sinon, elle applique la mise Ă jour et joue une courte animation d'entrĂ©e sur #root (ou le premier enfant de <body>).
  • Quand l'utilisateur a demandé au système de réduire les animations, il applique seulement la mise à jour - sans animation.
  • Les animations durent environ 240 ms.

appmintNative.unlock appmintNative#

unlock(title?, subtitle?) — from '@/lib/appmintNative'

Exemple

Demande au propriétaire du téléphone de prouver sa présence - empreinte, visage ou code PIN / code de verrouillage de l'appareil - et vous dit s'il y est parvenu.

Retourne : Promise<boolean> - true une fois déverrouillé ; false en cas d'annulation, d'échec, de blocage ou quand rien ne peut vérifier sur cet appareil. Ne lève jamais d'erreur. Nécessite : dans l'application, Empreinte digitale à l'étape 4 (Accès) lors de la compilation. Sur le web : l'authentificateur de plateforme du navigateur (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();
}

Masquez le bouton lĂ oĂč il n'y a rien pour dĂ©verrouiller :

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

const showLockButton = can('unlock') !== 'none';   // 'native' | 'web' | 'none'

Protéger une suppression par un nouveau déverrouillage, puis confirmer avec un retour haptique et 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');
}

Remarques : dans l'application, le code PIN, le schĂ©ma ou le mot de passe de l'appareil est autorisĂ© comme second moyen d'accĂšs. Sur le web, le premier appel enregistre l'authentificateur de plateforme du navigateur et compte comme dĂ©verrouillĂ©. Cela prouve que le propriĂ©taire de l'appareil est prĂ©sent ; ce n'est PAS une connexion - ne l'utilisez jamais comme seule protection de donnĂ©es hĂ©bergĂ©es sur un serveur. Sur Android 11 et plus rĂ©cent, l'invite s'ouvre sur tout tĂ©lĂ©phone ayant un verrouillage d'Ă©cran - un tĂ©lĂ©phone avec PIN seul l'a aussi, et se dĂ©verrouille avec le PIN ; sur Android 10 et plus ancien, une empreinte doit d'abord ĂȘtre enregistrĂ©e, sinon elle rĂ©pond false sans invite.

appmintNative.updates appmintNative#

updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()

Exemple

Un outil de mise à jour côté page pour les mises à jour de lots web. Le code de la page n'en a pas besoin : dans une application AppMint, la mise à jour à distance est faite nativement par l'application Android, et updates.supported() retourne false partout.

Retourne : supported() → boolean (actuellement toujours false) ; download(url, sha256, version, binaryVersion) → Promise<boolean> (false tant que non pris en charge) ; stage(dir, version, binaryVersion) → Promise<boolean> ; confirm() → void ; revertIfUnconfirmed(binaryVersion) → Promise<boolean> ; current() → { path, version, binaryVersion } | null. Nécessite : rien pour votre application - les mises à jour sont publiées depuis Remote Update dans AppMint.

Le seul usage sûr dans le code de l'application est de vérifier, et de ne rien faire quand c'est désactivé :

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

Affichez quel bundle est en cours d'exĂ©cution (une ligne « À propos » des paramĂštres) :

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

Afficher la version du contenu en cours dans une ligne de paramètres, avec le libellé intégré quand la page n'a pas de bundle préparé :

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

Remarques :

  • Dans l'application Android, le gestionnaire de mise à jour à distance de l'enveloppe télécharge, vérifie et sert les mises à jour, et annule un lot qui ne se charge pas. La page est servie par l'enveloppe elle-même, donc un lot préparé depuis le code de la page ne serait jamais affiché - c'est pourquoi supported() vaut false.
  • Sur le web, il n'y a rien à mettre à jour ; la page est ce que le serveur envoie.
  • Les mises à jour ne peuvent modifier que le HTML, le CSS et le JavaScript ; une nouvelle fonctionnalité native nécessite toujours une nouvelle compilation de l'application.

appmintNative.watchIdle appmintNative#

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

Exemple

Prévient l'application quand l'utilisateur devient inactif ou verrouille l'écran, et quand il revient (can('idle')). Lancez-la depuis un appui la premiÚre fois : la demande d'autorisation en exige un.

Retourne : une Promise de la fonction d'arrêt. Le callback reçoit { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } au démarrage et à chaque changement. Rejette avec code 'permission-denied' ou 'unavailable'. Nécessite : rien, aucune autorisation 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>
  );
}

Afficher un état d'absence et arrêter la surveillance au démontage ; une autorisation refusée est signalée avec son 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 };
}

Mettre en pause un minuteur d'interrogation tant que l'utilisateur est absent et actualiser Ă son retour :

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

Remarques : le seuil est d'au moins une minute (le minimum de la spécification).

appmintNative.widget appmintNative#

widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()

Exemple

Remplit le widget d'écran d'accueil de l'application - une carte avec un titre, une grande valeur, une légende, jusqu'à douze lignes (chacune avec une couleur, un marqueur et une route facultatifs), et un appui qui ouvre l'application sur une route.

Retourne : set(card) → Promise<boolean> (true une fois enregistré) ; clear() → Promise<void> ; placedCount() → Promise<number> (combien de copies l'utilisateur a placées) ; requestPin() → Promise<boolean> (true quand le parcours « ajouter un widget » du lanceur s'est ouvert). Aucune ne rejette. Nécessite : rien dans l'application Android (le widget fait toujours partie de la compilation).

Mettez à jour la carte aprÚs chaque changement que l'utilisateur voudrait voir sur l'écran d'accueil :

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

export async function updateTodayWidget(tasks: { title: string; done: boolean }[]) {
  if (can('widget') === 'none') return;          // on the web
  const open = tasks.filter((t) => !t.done);
  await widget.set({
    title: 'Today',
    value: `${open.length} left`,
    lines: open.slice(0, 6).map((t) => '• ' + t.title),
    route: '/today',                              // a tap opens the app here
  });
}

Un agenda qui remplit un widget redimensionné, avec un fond transparent et une ligne en retard en rouge :

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 bouton « Ajouter à l'écran d'accueil » :

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

Videz la carte quand l'utilisateur se déconnecte, et affichez combien de copies sont sur l'écran d'accueil :

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

Remarques :

  • La carte est enregistrée sur l'appareil, survit à un redémarrage, et chaque copie placée est actualisée. placedCount() de 0 signifie qu'aucune copie n'est encore sur l'écran d'accueil.
  • Champs de la carte : title (vide = le nom de l'application), value (affiché en grand dans accent, masqué quand vide), caption (petit texte sous la valeur), lines (12 max ; six s'affichent toujours, davantage quand le widget est assez haut ; une ligne est une chaîne ou { text, color?, marker?, bold?, wrap?, route? }), route (par défaut /), textSize ('auto' agrandit le texte avec le widget tant que chaque ligne tient encore, en renvoyant les longues lignes sur deux rangées ; 'small' | 'normal' | 'large' | 'xl' ou un nombre de 0,6 à 3), align ('start' | 'center' | 'end'), wrap (les longues lignes prennent deux rangées), et bg / ink / accent. Les couleurs prennent par défaut les jetons CSS --bg, --ink, --accent ; elles doivent être #RRGGBB, #AARRGGBB (alpha EN PREMIER : #9914264A est opaque à 60 %), #RGB, rgb()/rgba() ou un nom de couleur - une valeur hsl()/oklch() retombe sur les couleurs par défaut. set() retourne false pour un textSize/align inconnu ou une mauvaise couleur de ligne.
  • Un appui ouvre l'application avec appmint-shortcut://<package><route> ; lisez-le avec launchUrl() / onDeepLink().
  • requestPin() nécessite Android 8+ et un lanceur qui le prend en charge. clear() affiche la carte vide « Rien à afficher pour le moment ».
  • Application Android uniquement : can('widget') vaut 'none' sur le web, où set() et requestPin() retournent false.

Généré à partir de l'environnement d'exécution de l'application et de ses fichiers d'exemple à chaque compilation de la documentation. Lire en Markdown · Toutes les familles.

Vérifié par rapport au pont publié le 22/10/2026.