JavaScript bridge API
appmintNative (приложения, созданные ИИ)
Типизированные обёртки, которые каждый проект, созданный с помощью ИИ, импортирует из @/lib/appmintNative. Каждая выбирает нативный путь внутри приложения и веб-путь или честное none в браузере — один вызов работает везде.
appmintNative.actionSheet appmintNative#
actionSheet(title: string, options: { title: string; destructive?: boolean }[]): Promise<number | null>
Пример
Показывает нижнее окно «выберите одно из этого» — нативный элемент для меню действий строки.
Возвращает: Promise<number | null> — индекс выбранного варианта или null, когда пользователь закрыл окно (нажатие на фон, Escape, системная отмена). Закрытие — обычное дело, а не ошибка. Требуется: ничего.
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); }
}Открывайте из видимой кнопки и по долгому нажатию на строку:
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>;
}Форма в стиле Capacitor ActionSheet.showActions({ title?, options }) принимает те же параметры и возвращает { index }, с -1 при закрытии без выбора:
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)`Примечания:
- В приложении Android плагин Capacitor ActionSheet рисует системную панель;
destructiveиспользует её стиль для опасных действий. - В остальных случаях это настоящая панель в палитре приложения (
--surface,--ink,--danger) с затемнением, Escape, переносом фокуса в панель и тактильным откликом выбора. - Пустой список
optionsсразу завершаетсяnull. Для любого содержимого, кроме списка действий, используйтеsheet().
appmintNative.ActionSheet appmintNative#
import { ActionSheet } from '@/lib/appmintNative'
Пример
Описано вместе с appmintNative.actionSheet — пример там показывает и этот.
Показывает нижнее окно «выберите одно из этого» — нативный элемент для меню действий строки.
Возвращает: Promise<number | null> — индекс выбранного варианта или null, когда пользователь закрыл окно (нажатие на фон, Escape, системная отмена). Закрытие — обычное дело, а не ошибка. Требуется: ничего.
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); }
}Открывайте из видимой кнопки и по долгому нажатию на строку:
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>;
}Форма в стиле Capacitor ActionSheet.showActions({ title?, options }) принимает те же параметры и возвращает { index }, с -1 при закрытии без выбора:
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)`Примечания:
- В приложении Android плагин Capacitor ActionSheet рисует системную панель;
destructiveиспользует её стиль для опасных действий. - В остальных случаях это настоящая панель в палитре приложения (
--surface,--ink,--danger) с затемнением, Escape, переносом фокуса в панель и тактильным откликом выбора. - Пустой список
optionsсразу завершаетсяnull. Для любого содержимого, кроме списка действий, используйтеsheet().
appmintNative.addContact appmintNative#
addContact(contact: Contact): Promise<boolean>
Пример
Сохраняет новый контакт в адресную книгу телефона. Только приложение Android: мост оболочки сохраняет его напрямую (экран «Контакты» не открывается).
Возвращает: Promise<boolean> — true после сохранения, false, если пользователь отказал в разрешении на контакты или сохранить не удалось. Отклоняется с Error, у которого code равно 'not_enabled' (сборка не включила запись контактов) или 'foreign_origin', а также после 5 минут ожидания без ответа. Требуется: переключатель Редактирование контактов на шаге 4 (Доступ) — сборка включает его, когда ваш код вызывает это, — и разрешение пользователя при первом использовании.
Сохраните клиента из формы.
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
}
}Пропустите контакт, который уже сохранён. searchContacts читает адресную книгу, поэтому проверьте перед добавлением:
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';
}Отличите ошибку сборки от отказа. Отклонение с code 'foreign_origin' или 'not_enabled' — это проблема сборки, false — выбор пользователя:
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>;
}Примечания: Сохраняет сразу — редактор контакта не открывается, — поэтому сначала подтвердите это с пользователем на собственном экране. Телефоны сохраняются как мобильные номера, а контакт — на устройстве (без аккаунта). Запись контактов — чувствительная функция: укажите её в форме Data safety в Play и в политике конфиденциальности.
appmintNative.alarmSupport appmintNative#
alarmSupport() — @/lib/appmintNative
Пример
Может ли scheduleAlarm поставить будильник прямо сейчас и чего не хватает, если нет.
Возвращает: 'ready', 'not_enabled' (сборка без будильника), 'needs_exact_alarm' (Android 12+: точные будильники выключены), 'needs_notification_permission' (Android 13+) или 'unavailable' (веб и iOS — не рисуйте там элементы управления будильником). Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.
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>;
}Устанавливайте ежедневный будильник только когда телефон готов, а если нет — скажите почему:
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.';
}Скройте экран будильника в вебе и реагируйте, когда будильник закончился, в сочетании с 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)
Пример
Название приложения, id пакета и версия, способ закрыть приложение и события приложения — в форме плагина App от Capacitor.
Возвращает: getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → форма Capacitor (Promise дескриптора, у которого также есть remove()). События: appStateChange ({ isActive }), pause, resume (страница уходит в фон / возвращается из него), appUrlOpen ({ url }, deep link — то же, что onDeepLink), backButton (нажатие или жест «назад»; решает ваш обработчик — приложение само не закрывается). Нужно: ничего.
import { App } from '@/lib/appmintNative';
const info = await App.getInfo();
aboutLine.textContent = `${info.name} ${info.version}${info.build ? ` (${info.build})` : ''}`;Закройте приложение из пункта «Выйти», который не является единственным путём вперёд:
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();
}Покажите версию приложения на экране 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>;
}Примечания: В приложении getInfo читает плагин Capacitor App; когда его нет, используются имя и версия моста оболочки (id и build тогда равны ''), а в вебе — document.title с пустой версией. exitApp использует closeWindow оболочки в приложении и ничего не делает в вебе, поэтому никогда не делайте «Выйти» единственным путём вперёд. О событиях паузы/возобновления и deep link см. Capacitor.Plugins.App, launchUrl() и onDeepLink().
appmintNative.appLauncher appmintNative#
appLauncher.canOpen(url): Promise<boolean> / appLauncher.open(url): Promise<boolean>
Пример
Открывает другое приложение по ссылке — whatsapp://send?text=…, tel:, mailto:, geo: или ссылке https, которую ОС отдаёт своему приложению, — и сначала спрашивает, получится ли это.
Возвращает: canOpen() → Promise<boolean>. open() → Promise<boolean> (false, если ни одно приложение не приняло ссылку). Требуется: ничего.
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);
}Позвонить или написать письмо (проверка не нужна; на телефоне всегда есть приложение для этого):
import { appLauncher } from '@/lib/appmintNative';
await appLauncher.open('tel:+15551234567');
await appLauncher.open('mailto:[email protected]?subject=Order%2042');Откройте приложение карт по ссылке geo: и сообщите, если никто её не принял:
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.');
}Примечания: В приложении используется плагин Capacitor AppLauncher. На Android 11+ canOpen видит только схемы, объявленные приложением (https, geo, google.navigation, tel, mailto, sms, whatsapp, intent, upi); для остальных он отвечает false, даже если приложение установлено — вызовите open() и проверьте его результат. В вебе нестандартную схему проверить нельзя, поэтому canOpen равно true только для http(s), а open переходит по ссылке.
appmintNative.appLock appmintNative#
appLock({ timeoutSec?, title?, onLocked? }) — from '@/lib/appmintNative'
Пример
Блокирует приложение снова, когда пользователь возвращается после пребывания в фоне: запрашивает отпечаток / лицо / PIN (unlock()) и включает экран приватности, чтобы переключатель приложений не показывал ваше содержимое.
Возвращает: функцию, останавливающую блокировку (вызовите её при выходе из аккаунта или размонтировании). onLocked(true) срабатывает, когда приложение блокируется, onLocked(false) — после успешной разблокировки. Требуется: то же, что для unlock() — Android: Отпечаток пальца отмечен при сборке.
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;
}В корневом компоненте: const locked = useAppLock(); if (locked) return <LockScreen />;.
Остановите блокировку при выходе из аккаунта, чтобы вышедшего пользователя не просили разблокировать:
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;
}Предлагайте блокировку только там, где устройство может её снять, и сочетайте с unlock() для кнопки «Заблокировать сейчас»:
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>
</>
);
}Примечания: если на устройстве нечем разблокировать (can('unlock') === 'none'), ничего не происходит и возвращается пустая функция — блокировка, которую нельзя открыть, заперла бы пользователя. Если пользователь отменяет, запрос повторяется до пяти раз, затем остаётся заблокировано (держите свой экран блокировки), пока приложение не уйдёт в фон и не вернётся. Это отдельно от опции PIN-блокировка в мастере сборки, которую приложение показывает на уровне системы при запуске.
appmintNative.appUpdate appmintNative#
appUpdate.check(): Promise<AppUpdateInfo | null> / appUpdate.start(): Promise<boolean>
Пример
Проверяет в Google Play наличие новой версии и запускает встроенное обновление Play (скачивается и устанавливается на месте).
Возвращает: check() → Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null в вебе, без сервисов Play или когда Play даёт сбой). updateAvailable — собственный ответ магазина. start() → Promise<boolean>: true только когда обновление Play завершено, false, когда пользователь отменил, произошёл сбой или обновления не было. Требуется: ничего; приложение должно быть установлено из 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);
});
}Сравните код версии в Play с установленным и считайте null признаком «здесь проверить нельзя»:
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}).`;
}Баннер React, который проверяет один раз при открытии экрана:
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>;
}Примечания: updateAvailable — собственный ответ Play; inPlace добавляет, что Play может выполнить своё обновление в приложении прямо сейчас. availableVersion — КОД версии в Play (названия Play не даёт): сравнивайте его с currentVersionCode, а не с currentVersion, то есть названием версии.
appmintNative.askForReview appmintNative#
askForReview(options): Promise<ReviewOutcome>
Пример
Просит Google Play показать собственное окно оценки внутри приложения и сообщает, что произошло.
Возвращает: Promise<ReviewOutcome> — { result, openedStore, message? } с result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' в вебе). { openStore: true } открывает страницу в Play, если окно не показалось. Нужно: ничего; окно появляется только у приложения, установленного из 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!');
}Скажите пользователю, что произошло, для каждого исхода:
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}` : ''}.`;
}
}Спросите один раз, после того как пользователь завершит третью задачу, и запомните, что спрашивали:
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();
}Примечания: Просите после удачного момента и редко. Тестовая установка вручную всегда отвечает not_from_play.
appmintNative.backgroundLocation appmintNative#
import { backgroundLocation } from '@/lib/appmintNative'
Пример
Местоположение, которое продолжает поступать, пока приложение в фоне: start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.
Возвращает: start(options) → Promise<BgResult> ({ ok, background, backgroundPermission, precise } или { ok: false, reason }). stop / clearBuffered → Promise<boolean>. buffered() → Promise<LocationFix[]>. requestAlways() → Promise<{ foreground, background, precise, error? }>. onFix(cb) → функцию отписки; cb получает каждую деталь appmint:location. Требуется: Фоновое местоположение в сборке; разрешение на местоположение (сначала запросите через 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(); };
}Попросите «Разрешить всегда» перед запуском и объясните отказ:
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}).`;
}Хук React, который показывает живые точки и прекращает слушать при закрытии экрана:
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'
Пример
Нативные HTTP-запросы, которые выполняются при закрытом приложении: enqueue (разовое надёжное автосохранение, отправляемое при появлении сети), schedule (повторяющийся запрос, не чаще чем раз в 15 минут), cancel и list.
Возвращает: enqueue / schedule → Promise<boolean>; cancel → Promise<void>; list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } — pendingDelivery на Android: разовая задача, ещё не доставленная). Нужно: в приложении Android ничего (используется WorkManager). В вебе can('backgroundSync') равно 'none', а enqueue / schedule завершаются false.
Надёжное автосохранение: тот же id заменяет сохранение, которое ещё не отправлено.
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),
});
}Повторяющаяся синхронизация, список и отмена:
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');Необработанное событие (чего ждёт обёртка на 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);
});Примечания: true означает, что задание поставлено в очередь, но не доставлено. false означает, что оно не поставлено в очередь: оболочка ответила status: 'failed' (URL не начинается с http, пустой id или система отказала) либо не ответила в течение 10 секунд. JavaScript страницы никогда не выполняется в фоне; выполняется только этот запрос. Ответ 4xx не повторяется; сетевые ошибки и 5xx повторяются до 8 раз.
appmintNative.backgroundTasks appmintNative#
import { backgroundTasks } from '@/lib/appmintNative'
Пример
Собственный код страницы, запускаемый периодически при закрытом приложении (типизированная обёртка над AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.
Возвращает: register / runNow → Promise<BgResult>; unregister / done → Promise<boolean>; list() → зарегистрированные задачи с lastRunAt / lastResult; isBackgroundRun() / currentRun() синхронны. Требуется: Выполнение кода страницы при закрытом приложении в сборке.
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();
}Регистрируйте при входе, удаляйте при выходе и выводите список зарегистрированного:
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.';
}Запустите задачу прямо сейчас по кнопке «Синхронизировать» и проверьте ответ:
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'
Пример
Устанавливает или сбрасывает счётчик непрочитанного на значке приложения в лаунчере.
Возвращает: badge.set(count) → Promise<boolean> (false, если плагина Badge нет, например в вебе); badge.clear() → Promise<void>. Нужно: на Android ничего (используется встроенный плагин Badge).
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();
}Отражайте счётчик непрочитанных, когда push-сообщение приходит при открытом приложении:
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);
}Отметьте всё прочитанным и сбросьте счётчик вместе; пропустите значок там, где его нет:
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;
}Примечания: на Android вид зависит от лаунчера: многие показывают число, некоторые только точку, некоторые ничего — не размещайте информацию только в значке. В вебе can('badge') — 'none'.
appmintNative.barcodeFormats appmintNative#
barcodeFormats(): Promise<string[]>
Пример
Перечисляет форматы штрихкодов и QR, которые может прочитать BarcodeDetector этого устройства.
Возвращает: Promise<string[]> — названия форматов вроде 'qr_code', 'ean_13', 'code_128'; пустой массив, когда чтение штрихкодов здесь недоступно. Никогда не отклоняется. Требуется: ничего.
Показывайте вариант «Сканировать штрихкод товара» только когда устройство читает этот формат:
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;
}Сузьте сканирование до того, что поддерживает устройство:
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)));Создайте выбор формата, в котором только то, что это устройство может прочитать:
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>
);
}Примечания:
- Он обращается к
BarcodeDetector.getSupportedFormats()движка браузера. В WebView приложения Android он есть (проверено: aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e); во многих настольных браузерах и в Safari его нет, и они отвечают[]. - Этот список касается
readBarcodeFrom()/scanBarcode().scanCode()в приложении использует сканер Google и от него не зависит.
appmintNative.batteryOptimization appmintNative#
import { batteryOptimization } from '@/lib/appmintNative'
Пример
Исключение из оптимизации батареи: isIgnoring() и request().
Возвращает: isIgnoring() → boolean. request() → Promise<string>: 'already_ignoring', 'dialog_opened' (опция сборки «Запрашивать отключение оптимизации батареи напрямую»), 'settings_opened', 'unavailable' или 'unsupported' вне приложения для Android. Требуется: ничего для списка настроек.
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;
}Покажите текущее состояние в строке настроек:
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>
);
}Запросите исключение перед запуском долгой фоновой задачи, вместе с 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'
Пример
Стандартные фитнес-датчики Bluetooth — пульс, скорость/каденс велосипеда, мощность, скорость/каденс бега и заряд датчика — с уже разобранными числами, в приложении для Android.
Возвращает: каждый метод возвращает Promise<void> (setWheelCircumference возвращает void). Всё остальное приходит как события окна appmint:ble с detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. Нужно: Bluetooth, который сборка включает сама, когда код использует bleSensors. В браузере (веб-предпросмотр) моста датчиков нет: startScan() отвечает {kind:'error', error:'not_enabled'}, так что обработайте эту ошибку. Чтобы узнать заранее, проверьте can('bluetooth') === 'native' — а не !== 'none': в Chrome can('bluetooth') равно 'web' (Web Bluetooth), который bleSensors не использует.
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>
);
}Откройте экран только в установленном приложении.
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>;
}Остановите сканирование на первом датчике и следите за скоростью вращения педалей.
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>;
}Примечания: сканирование длится 15 секунд (stopScan() завершает его раньше); одновременно подключён один датчик. Коды ошибок: not_enabled, permission_denied, bluetooth_unavailable, bluetooth_off, scan_failed, invalid_address. Для скорости нужны два пакета и размер колеса (по умолчанию 2096 мм). Те же сообщения приходят и в window.onAppMintBle; слушать событие проще. Для любого другого устройства Bluetooth используйте navigator.bluetooth.
appmintNative.brightness appmintNative#
brightness.get() / brightness.set(value) / brightness.restore()
Пример
Делает экран ярче для посадочного талона или QR-кода, затем возвращает управление системе. Только нативно — веб-страница не может управлять подсветкой.
Возвращает: get() → Promise<number | null> (0–1, пока приложение переопределяет уровень; -1, когда приложение следует системному уровню; null в вебе или когда его нельзя прочитать). set(value) → Promise<boolean> (значение ограничивается 0–1; false в вебе). restore() → Promise<void>. Нужно: ничего.
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 />;
}Запомните уровень, увеличьте яркость, затем верните обратно.
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;
}Держите экран ярким и включённым, пока показан код.
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]);
}Примечания: меняется только окно этого приложения, а не настройка яркости телефона, и переопределение заканчивается при закрытии приложения. restore() задаёт -1 (следовать системе). get() читает переопределение окна, поэтому оно -1 до вызова set() — это не системный уровень.
appmintNative.browser appmintNative#
browser.open(url, { toolbarColor? }) / browser.close() / browser.onClosed(cb)
Пример
Открывает страницу во вкладке встроенного браузера (Chrome Custom Tabs) — для условий, справки или входа — и в новой вкладке в вебе.
Возвращает: open() → Promise<boolean> (false, если вкладку не удалось открыть). close() → Promise<void>. onClosed(cb) → функция отписки; cb срабатывает, когда пользователь закрывает вкладку (только в приложении). Требуется: ничего.
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.'); }
}Ссылка на условия в компоненте 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>
);
}Закройте вкладку из кода и обновите данные, когда пользователь вернётся.
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
};
}Примечания: toolbarColor по умолчанию берёт токен --bg приложения. Вкладка — это отдельный браузер: у неё нет общих с приложением cookie и localStorage. close() закрывает вкладку из кода в приложении; в вебе он ничего не делает. onClosed в вебе никогда не срабатывает.
appmintNative.calendar appmintNative#
calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar
Пример
Добавляет, показывает и удаляет события в СОБСТВЕННОМ календаре телефона пользователя (поставщик календаря Android) и открывает приложение календаря на дате.
Возвращает: requestPermission() → Promise<'granted' | 'denied'>. createEvent(input) → Promise<string | null> (идентификатор события; null, когда пользователь отказал в доступе к календарю — отклоняется, когда само событие не удалось создать, например из-за неверных дат). listEvents({ from, to }) → Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (время в мс эпохи). deleteEvent(id) / openCalendar(date?) → Promise<boolean>. Отклоняется только createEvent, и только для события, которое не удалось создать. Требуется: включите Календарь на шаге 4 (Доступ) при сборке — сборка отмечает его сама, когда видит calendar.createEvent(, calendar.listEvents( или calendar.requestPermission( в коде.
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);
}Список за эту неделю, удаление, открытие:
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());Добавляйте в календарь, только когда запись возможна.
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;
}Примечания: в вебе createEvent скачивает файл .ics, который пользователь открывает в своём календаре (возвращает его UID), listEvents возвращает [] (can('calendarRead') равно 'none'), а deleteEvent / openCalendar возвращают false. Напоминания и повторяющиеся события доступны через Capacitor.Plugins.Calendar, а не через эту обёртку.
appmintNative.camera appmintNative#
camera.take(opts?) / camera.pick(opts?)
Пример
Делает фото камерой или выбирает фото из галереи: плагин Capacitor Camera в приложении, поле выбора файла с capture в вебе.
Возвращает: take() → Promise<Photo | null> с { dataUrl, mime }; null при отмене или отказе в разрешении. pick() → Promise<Photo[]>, пустой при отмене. Нужно: Камера на шаге 4 (Доступ) — сборка включает её сама, когда ваш код вызывает camera.take.
Сделайте фото профиля фронтальной камерой.
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;
}Выберите одно фото из галереи через take или несколько через 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);Загрузите фото, превратив data URL в 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 });
}Примечания: Параметры: source ('camera' по умолчанию, или 'photos'), quality 0–100 (по умолчанию 80), direction ('rear' по умолчанию, или 'front'), allowEditing. Фото не сохраняются в галерею. Отказ в разрешении возвращает null / [] и не бросает исключение — покажите короткую подсказку, чтобы пользователь понял, почему ничего не произошло. Камера — чувствительная функция: укажите её в форме безопасности данных Play.
appmintNative.can appmintNative#
can(cap: Capability): 'native' | 'web' | 'none'
Пример
Сообщает, где функция действительно будет работать на текущей платформе, чтобы приложение рисовало только рабочие элементы управления.
Возвращает: строку синхронно: 'native' (собственная реализация приложения — мост оболочки или плагин Capacitor, встроенный в приложение), 'web' (настоящий веб-стандарт в этом браузере) или 'none' (здесь недоступно — не отрисовывайте элемент управления). Нужно: ничего.
Скройте элемент управления, который не может работать на этой платформе:
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>;
}Меняйте формулировку в зависимости от платформы:
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';Названия возможностей: 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.
Покажите подсказку вместо неработающего элемента и превращайте функцию в простое «да/нет» с помощью 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';Примечания:
- Плагин Capacitor, встроенный в приложение, всегда отвечает
'native'; иначе проверяется мост оболочки, затем веб-стандарт. toast,actionSheet,dialog,statusBarиswipeимеют настоящую веб-версию, поэтому в браузере они не'none'.contactList,contactAdd,smsSend,smsList,callLog,shortcuts,shareTargetиwidgetв вебе равны'none'.- Некоторые функции существуют только если сборка их включила (доступ к папкам, защищённые ключи, управление медиа, SMS, контакты). Спрашивайте
can(), а неisNativeApp(). - Неизвестное имя возвращает
'none'. Для простого «да/нет» используйтеhas(cap).
appmintNative.clearPushInbox appmintNative#
import { clearPushInbox } from '@/lib/appmintNative'
Пример
Очищает входящие AppMint Push на этом телефоне. Безвозвратно — отмены нет.
Возвращает: boolean синхронно — true, если очищено; в вебе всегда false. Нужно: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
import { clearPushInbox, pushInbox } from '@/lib/appmintNative';
export async function onClearAll(confirmDelete: () => Promise<boolean>) {
if (!(await confirmDelete())) return pushInbox();
clearPushInbox();
return pushInbox(); // []
}Показывайте «Очистить» только когда есть что очищать, и сообщайте результат:
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>
);
}Отметьте всё прочитанным вместо удаления — так ничего не теряется:
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;
}Примечания: уведомления в шторке и история отправок в AppMint не затрагиваются; новые сообщения по-прежнему приходят.
appmintNative.click appmintNative#
click() — @/lib/appmintNative
Пример
Один раз проигрывает звук нажатия приложения для ваших собственных кнопок.
Возвращает: void. Требуется: включённый Звук нажатия на шаге 3 (Интеграция) при сборке. В вебе и в приложениях, собранных без звука нажатия, ничего не делает.
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>
);
}Позвольте пользователю отключить звук в ваших собственных настройках:
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
}Воспроизводите из строки списка вместе со всплывающим сообщением:
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>;
}Примечания: С параметром Следовать настройке телефона (рекомендуется) щелчок беззвучен, если пользователь выключил «Звуки нажатия» на телефоне. С Всегда воспроизводить при каждом нажатии каждое нажатие уже щёлкает, поэтому дополнительные вызовы click() не нужны. Только Android (оболочка WebToApk.playClick).
appmintNative.Clipboard appmintNative#
Clipboard.write({ string?, url? }) / Clipboard.read(): Promise<{ value, type }>
Пример
Буфер обмена в форме плагина Clipboard из Capacitor — тот же код, что и copy() / paste().
Возвращает: write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value равно '', если буфер пуст). Нужно: ничего.
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
}
}Скопируйте код и подтвердите всплывающим сообщением:
import { Clipboard, toast } from '@/lib/appmintNative';
export async function copyCode(code: string) {
await Clipboard.write({ string: code });
toast('Code copied');
}Запишите ссылку, когда текста нет, и считайте пустой буфер обмена отсутствием вставляемого:
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
}
}Примечания: В приложении используется плагин Capacitor Clipboard; в браузере — navigator.clipboard. write отправляет только string (или url, когда string пуста) — без изображений. В браузере read() может выбросить ошибку, если пользователь запретил доступ; в приложении пустой буфер даёт '', а не ошибку. Спросите can('clipboardRead'), прежде чем показывать элемент «Вставить».
appmintNative.collapsingHeader appmintNative#
collapsingHeader(header: HTMLElement, opts?: { scroller?: HTMLElement | Window; range?: number }): () => void
Пример
Заставляет заголовок с крупным названием реагировать на прокрутку, как нативные, публикуя значение от 0 до 1, которое можно использовать в CSS.
Возвращает: функцию отписки, которая также удаляет переменную и атрибут. Нужно: ничего.
Задаёт --collapse (от 0 вверху до 1 после range px прокрутки) у заголовка и добавляет data-collapsed, когда он полностью свёрнут:
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); }Остановите прослушивание из обычного скрипта; это также удаляет --collapse и 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);Примечания:
- По умолчанию
scroller— окно (страница, прокручиваемая целиком); по умолчаниюrange— 64 px. - Слушатель прокрутки пассивный и обновляется раз за кадр анимации, поэтому не замедляет прокрутку.
appmintNative.composeSms appmintNative#
composeSms(phoneNumber: string, message: string): boolean
Пример
Открывает приложение для сообщений с уже заполненными номером и текстом; пользователь нажимает «отправить». Работает в приложении Android (мост оболочки) и в вебе (ссылка sms:).
Возвращает: boolean сразу — true, если приложение для сообщений было запрошено на открытие. Нужно: разрешение не требуется.
Кнопка «Написать в магазин».
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.');
});
}Переиспользуемый хук, который скрывает кнопку, когда числа нет.
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}.`),
};
}Пригласите друга; пользователь нажимает «отправить».
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');
}Примечания: Предпочитайте это вместо sendSms() для всего, что пользователь пишет или одобряет, — ничто не покидает телефон, пока пользователь не нажмёт «Отправить». В вебе происходит переход на sms:…?body=…. Вызов composeSms( не включает SMS (чтение и отправка) при сборке, поэтому не добавляет ограниченное Play разрешение.
appmintNative.copy appmintNative#
copy(text: string): Promise<boolean>
Пример
Помещает текст в системный буфер обмена.
Возвращает: Promise<boolean> — true, если текст скопирован, false, если буфер обмена недоступен или браузер отказал. Требуется: ничего.
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>
);
}Форма в стиле Capacitor делает то же самое:
import { Clipboard } from '@/lib/appmintNative';
await Clipboard.write({ string: 'ABC-123' });Сообщите об обоих исходах, с тактильной отдачей только при успехе:
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;
}Примечания: в приложении текст записывает плагин Capacitor Clipboard (ошибка плагина отклоняет промис, а не возвращает false). В браузере используется navigator.clipboard.writeText, для которого нужна страница https и обычно нажатие пользователя.
appmintNative.copyImage appmintNative#
import { copyImage } from '@/lib/appmintNative'
Пример
Копирует изображение в буфер обмена, чтобы его можно было вставить в мессенджеры и почту: в буфер приложения в приложении Android, через Async Clipboard API в вебе.
Возвращает: Promise<boolean> — true после копирования. Нужно: ничего; ограничьте элемент управления через 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');
}Скопируйте фото с камеры, затем подтвердите это всплывающим сообщением.
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');
}Показывайте кнопку только там, где буфер обмена принимает изображения, и копируйте изображение по 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>
);
}Примечания: PNG — самый надёжный тип везде; приложение для Android также принимает JPEG и WebP.
appmintNative.Device appmintNative#
Device.getInfo() / Device.getId() / Device.getLanguageCode()
Пример
Форма @capacitor/device: модель телефона, версия ОС, постоянный ID и язык. В приложении используется плагин Capacitor Device, в вебе — ответ браузера.
Возвращает: getInfo() → Promise<DeviceInfo & { operatingSystem, isVirtual }> (с Capacitor также manufacturer, webViewVersion, …); getId() → Promise<{ identifier }>; getLanguageCode() → Promise<{ value }> (две буквы, например "en"). Нужно: ничего.
Покажите модель и версию ОС на экране «О приложении».
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' : '');
}Стабильный ID и код языка.
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 }),
});Покажите версию приложения и модель устройства в нижнем колонтитуле настроек. Отчёт синхронизации даёт версию приложения; плагин добавляет операционную систему.
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}`;
}Примечания: В приложении deviceInfo() синхронизации заполняет также model и osVersion; Device.getInfo() добавляет operatingSystem, isVirtual и дополнительные поля плагина. В вебе operatingSystem равно 'unknown', а model пусто. getId() в вебе — это ID localStorage из installId(), и он выбрасывает ошибку, когда хранилище заблокировано.
appmintNative.deviceInfo appmintNative#
deviceInfo(): DeviceInfo
Пример
Синхронно сообщает, где работает приложение (platform) и язык, а внутри приложения для Android — ещё и полный отчёт оболочки Android об устройстве.
Возвращает: DeviceInfo сразу: всегда { platform, language } — platform равно 'android' в приложении и 'web' в браузере. В приложении для Android также заполняет model, manufacturer, osVersion (например, "14") и appVersion (например, "1.4.0") и несёт каждый ключ WebToApk.getDeviceInfo(): android, hardware, screen, app, runtime, installId. В вебе добавляет osVersion (user agent). Требуется: ничего.
Ветвитесь по платформе во время отрисовки.
import { deviceInfo } from '@/lib/appmintNative';
const info = deviceInfo();
const isAndroidApp = info.platform === 'android';
const lang = info.language; // e.g. "en-IN"Прочитайте сведения об Android. Основные поля находятся вверху; остальное из отчёта оболочки остаётся во вложенных группах.
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}%`);
}Выберите нужный элемент управления по платформе.
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>;
}Примечания: model и appVersion заполняются только в приложении. В вебе osVersion — это строка user-agent браузера, а не версия ОС. Выбрасывает ошибку, если ответ оболочки — не корректный JSON (настоящий дефект, не ожидается).
appmintNative.Dialog appmintNative#
Dialog.alert / Dialog.confirm / Dialog.prompt
Пример
Настоящие системные диалоги: сообщение, вопрос «да/нет», вопрос с однострочным ответом. Приложение использует плагин Capacitor Dialog; веб — собственные средства браузера alert / confirm / prompt.
Возвращает: alert({ title?, message }) → Promise<void>. confirm({ title?, message, okButtonTitle?, cancelButtonTitle? }) → Promise<{ value: boolean }>. prompt({ title?, message, inputPlaceholder?, inputText? }) → Promise<{ value: string; cancelled: boolean }>. confirm и prompt никогда не отклоняются (закрытие — это ответ); alert отклоняется только когда его нельзя показать (нет сообщения). Требуется: ничего.
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);
}Alert и prompt:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Offline', message: 'Your changes will sync when you are back online.' });
const r = await Dialog.prompt({ title: 'Rename', message: 'New name', inputText: current, inputPlaceholder: 'List name' });
if (!r.cancelled && r.value.trim()) rename(r.value.trim());Подтвердите, прежде чем выйти из аккаунта. Закрытие диалога считается ответом «нет».
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');
}Примечания: Закрытие confirm даёт { value: false }; закрытие prompt даёт { value: '', cancelled: true }. Диалоги блокируют экран — используйте их для настоящих решений, а toast() — для сообщений вроде «Сохранено». prompt не передаёт плагину свои названия кнопок.
appmintNative.dismissPushMessage appmintNative#
import { dismissPushMessage } from '@/lib/appmintNative'
Пример
Скрывает одно сообщение из входящих AppMint Push (смахивание). Отмените это через restorePushMessage(id).
Возвращает: boolean синхронно — true, если было скрыто. В вебе всегда false. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
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());
});
}Покажите входящие с обработчиком смахивания и состоянием отмены, чтобы смахивание никогда не было окончательным.
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>
);
}Скройте все прочитанные сообщения сразу и сообщите, сколько скрыто.
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;
}Примечания: меняется только входящие на этом телефоне; уведомление в шторке и история отправок остаются.
appmintNative.docScanner appmintNative#
docScanner.scan({ pageLimit?, pdf?, quality? }): Promise<{ pages, pdfPath? } | null> / docScanner.isSupported(): Promise<boolean>
Пример
Открывает системный сканер документов — определение краёв, выравнивание, несколько страниц — сканер Google ML Kit в приложении для Android. Страницы возвращаются как JPEG data URL.
Возвращает: Promise<{ pages: string[]; pdfPath?: string } | null> — null, когда пользователь отменяет, сканер не может запуститься или в вебе. isSupported() → Promise<boolean>: может ли этот телефон сканировать (сервисы Google Play); false в вебе. Требуется: ничего включать не нужно (на Android сканер работает в сервисах Google Play и не требует разрешения камеры).
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>
);
}Сначала спросите телефон, потому что на старом устройстве сканера может не быть, а затем сохраните страницы.
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} />)}
</>
);
}Отсканируйте, затем загрузите первую страницу с помощью помощника передачи файлов.
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;
}Примечания: pageLimit 0 (по умолчанию) = без ограничений; quality 0,1–1 (по умолчанию 0,85). С pdf: true pdfPath — это путь к файлу в кэше приложения, а не URL, который страница может загрузить. Data URL большие — уменьшайте их или загружайте, а не храните много в localStorage. Для обычного фото используйте camera.take().
appmintNative.enableNotifications appmintNative#
enableNotifications(): Promise<EnableNotificationsResult>
Пример
Возвращает пользователя, который отклонил уведомления или пропустил их, по нажатию на кнопку: диалог разрешения, пока его ещё можно показать, иначе (приложение Android) — настройки уведомлений этого приложения.
Возвращает: Promise<{ state, changed, openedSettings }> — state равно 'granted' | 'prompt' | 'blocked' | 'unavailable', changed — true, если оно отличается от значения до вызова, openedSettings — true, если использовался экран настроек. Никогда не отклоняется. В вебе запрашивает через Notification.requestPermission(). Нужно: уведомления в сборке (они включены всякий раз, когда приложение отправляет или получает уведомления).
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';
});
}Примечания: показывайте баннер только для 'prompt' и 'blocked'; скрывайте для 'granted' и 'unavailable'. Спрашивайте по нажатию, никогда при запуске.
appmintNative.fileTransfer appmintNative#
fileTransfer.download / fileTransfer.upload from '@/lib/appmintNative'
Пример
Скачивает или загружает файл с функцией отслеживания прогресса. В приложении передача идёт нативно (Capacitor FileTransfer); в вебе используются fetch / XMLHttpRequest с той же функцией.
Возвращает: download(url, fileName, opts?) → Promise<{ path?: string; blob?: Blob }> — path (файл в кэше приложения) в приложении, blob в вебе (где файл также сохраняется через браузер). upload(url, file, opts?) → Promise<{ responseCode: number; responseBody: string }>. upload завершается с любым HTTP-статусом, который ответил сервер (проверяйте responseCode), и в приложении, и в вебе; оба отклоняются, если сама передача не удалась (нет соединения, неверный файл). Требуется: ничего включать не нужно.
Скачивание с индикатором выполнения:
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)}`);
}
}Загрузите File из <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})`);
}Загрузка с состоянием прогресса, а затем открытие файла в одном хуке.
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 };
}Примечания: fraction равно 0, пока сервер не присылает длину. В приложении загрузка попадает в папку КЭША приложения (не в «Загрузки» пользователя); чтобы дать пользователю копию, используйте saveFile. Для отправки приложение сначала копирует файл в свой кэш, поэтому очень большим файлам нужно столько же свободного места. method (по умолчанию 'POST') и fileName — необязательные параметры отправки.
appmintNative.fileViewer appmintNative#
fileViewer.open(target) from '@/lib/appmintNative'
Пример
Открывает документ (PDF, изображение, офисный файл) в приложении-просмотрщике телефона. В вебе открывает адрес в новой вкладке.
Возвращает: Promise<boolean> — true, когда программа просмотра открыта, false, когда не удалось (нет приложения для этого типа, неверный путь) или цель нельзя открыть на этой платформе. Требуется: ничего включать не нужно; используется плагин Capacitor FileViewer, входящий в приложение.
Откройте документ из веба:
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.');
}Откройте файл, сначала скачанный через fileTransfer.download (локальный путь в приложении):
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Откройте ссылку по кнопке и сообщите пользователю, если никакая программа просмотра не смогла её открыть.
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>
);
}Примечания: цель http(s) использует openDocumentFromUrl; всё остальное считается локальным путём (file:// отбрасывается). В вебе открываются только цели http(s):, blob: и data:; локальный путь отвечает false.
appmintNative.folder appmintNative#
import { folder } from '@/lib/appmintNative'
Пример
Целая папка, доступ к которой пользователь даёт один раз, а приложение сохраняет его между запусками, с настоящими файловыми операциями внутри (в приложении: мост папок оболочки).
Возвращает: каждый член асинхронный — connected() → boolean, label() → string, request() → boolean (false при отмене), disconnect() → void, list(path) → [{ name, uri, isDirectory, length, lastModified }], readText(path) → string | null (null = файла нет), writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean, stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. Нужно: приложение, созданное ИИ, само включает переключатель Native Folder Access (SAF), когда код использует folder; при ручной сборке отметьте его на шаге 4 (Доступ).
Подключите, покажите папку и отключите:
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();
}Чтение, запись, список, переименование и удаление — каждый путь задаётся относительно выданной папки ('' — её корень):
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Сохраните заметку в выданную папку и прямо скажите, если папка не подключена.
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');
}Примечания: can('folderAccess') равно 'none' в вебе — в браузере телефона нет аналога постоянной папки, поэтому не показывайте там эту функцию (для отдельных файлов используйте pickFile() / saveFile()). Вызывайте request() только по нажатию пользователя. .. и абсолютные пути отклоняются. Саму выданную папку удалить нельзя. stat() работает для файлов и вложенных папок (у папки есть isDirectory: true и size: 0); stat('') — корень — это null, для него используйте label(). request() можно вызвать снова после отмены.
appmintNative.fullscreen appmintNative#
fullscreen.enter() / fullscreen.exit() / fullscreen.isOn()
Пример
Скрывает системные панели (в приложении) или включает полноэкранный режим браузера (в вебе) — для видео, игры или режима чтения.
Возвращает: enter() и exit() возвращают Promise<void> и никогда не отклоняются; isOn() синхронно возвращает boolean. Требуется: ничего.
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>
);
}Выходите из полноэкранного режима, когда представление закрывается:
import { useEffect } from 'react';
import { fullscreen } from '@/lib/appmintNative';
export function useFullscreenWhileOpen() {
useEffect(() => {
void fullscreen.enter();
return () => { void fullscreen.exit(); };
}, []);
}Скройте системные панели для режима чтения и следите за состоянием через кнопку.
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 };
}Примечания:
- В приложении это собственный полноэкранный режим оболочки (панели состояния и навигации скрываются); он всегда доступен странице.
- В вебе Fullscreen API работает только по нажатию пользователя. Отклонённый запрос молча игнорируется, поэтому после него читайте
isOn(), а не предполагайте.
appmintNative.geofences appmintNative#
import { geofences } from '@/lib/appmintNative'
Пример
Геозоны, которые срабатывают при закрытом приложении и переживают перезагрузки: add, remove, list, events, clearEvents, onEvent, available.
Возвращает: add(spec) → Promise<BgResult>, завершается, когда сервисы Play приняли или отклонили ({ id, ok, reason? } — background_permission_required, location_off, too_many_geofences, …). remove / clearEvents → Promise<boolean>. list() → Promise<GeofenceSpec[]>. events() → Promise<GeofenceEvent[]> (последние 100, включая происшедшие при закрытом приложении). onEvent(cb) → отписка. Требуется: Геозоны в сборке и «Разрешить всегда» (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)));Перечислите активные геозоны и переходы, записанные, пока приложение было закрыто.
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()}`),
];
}Добавьте рабочую зону для входа и выхода, обработайте отказ по его причине и удалите её позже.
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
Пример
Форма @capacitor/geolocation: плагин Capacitor Geolocation в приложении, стандартный navigator.geolocation в вебе. Все возвращают объект позиции W3C.
Возвращает: getCurrentPosition(opts) → Promise<GeolocationPosition> (отклоняется при отказе, тайм-ауте или отсутствии местоположения); watchPosition(opts, cb) → Promise<string> (id наблюдения); clearWatch({ id }) → Promise<void>; checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (каждое 'granted' | 'denied' | 'prompt'; на Android 12+ пользователь, выбравший «Приблизительное», даёт только coarseLocation). Требуется: GPS / Местоположение на шаге 4 (Доступ) — сборка включает оба, когда ваш код вызывает getCurrentPosition или watchPosition.
Где я, один раз.
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?';
}
}Отслеживайте маршрут, затем остановите. Обратный вызов получает null и ошибку, если определение местоположения не удалось.
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; }
}Получите разовое положение. Возвращает null, когда пользователь отказывает.
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
}
}Примечания: Обычный navigator.geolocation тоже работает в приложении; используйте это, когда нужна форма Capacitor. На Android 12+ пользователь может разрешить только приблизительное местоположение — позиция всё равно будет, просто менее точная. В вебе requestPermissions() делает один настоящий запрос позиции, чтобы вызвать запрос в браузере. Местоположение — личные данные: укажите его в форме Data safety в Play.
appmintNative.getNotificationState appmintNative#
getNotificationState(): Promise<NotificationState>
Пример
Включены ли уведомления, а если нет — может ли приложение ещё спросить или исправить это можно только в настройках.
Возвращает: Promise<'granted' | 'prompt' | 'blocked' | 'unavailable'>. В вебе Notification.permission сопоставляется так же (default → 'prompt', denied → 'blocked'); 'unavailable' там, где уведомлений нет вообще. Требуется: ничего.
import { getNotificationState } from '@/lib/appmintNative';
export async function shouldShowNotifBanner(): Promise<boolean> {
const state = await getNotificationState();
return state === 'prompt' || state === 'blocked';
}Примечания: проверьте снова, когда приложение возвращается на передний план (visibilitychange) — пользователь мог изменить это в настройках.
appmintNative.getPushTags appmintNative#
import { getPushTags } from '@/lib/appmintNative'
Пример
Читает теги AppMint Push этой установки в виде объекта.
Возвращает: Record<string, string> синхронно — например { plan: 'pro', cart: 'open' }; {}, когда их нет и в вебе. Требуется: Android: AppMint Push, включённый на шаге «Интеграция» (шаг 3) при сборке.
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');Задайте несколько тегов за одно изменение с помощью setPushTags, когда экран меняет больше одного, и очистите значение, чтобы убрать тег:
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)');
}Прочитайте теги обратно, чтобы показывать подсказку только когда у этой установки есть тег:
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>;
}Примечания: это то, что хранит телефон; центр отправки увидит это после следующей проверки связи этой установки.
appmintNative.getPushUser appmintNative#
import { getPushUser } from '@/lib/appmintNative'
Пример
Читает идентификатор пользователя, который эта установка в последний раз передала AppMint Push через setPushUser.
Возвращает: string синхронно — сохранённый id или '', если вход не выполнен, id не задан или в вебе. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
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);
}Вызовите setPushUser с id аккаунта после входа и с '' при выходе; он возвращает false, если id отклонён или AppMint Push выключен:
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('');
}Прочитайте сохранённый id в хуке React вместе с тегом, который принадлежит этому пользователю:
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;
}Примечания: это то, что хранит телефон; центр отправки видит изменение после следующей проверки связи установки — через несколько секунд, когда устройство в сети.
appmintNative.getVolume appmintNative#
getVolume(stream) — @/lib/appmintNative
Пример
Читает уровень громкости самого телефона для одного потока: 'media', 'alarm', 'ring', 'notification' или 'call'.
Возвращает: { stream, current, max, percent, muted? } (current/max в собственных шагах телефона, percent 0–100) или null в вебе. Требуется: ничего.
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 в вебе отвечает null, поэтому рисуйте уровень, только когда это объект:
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" />;
}Следите за аппаратными клавишами через onVolumeChange и повышайте тихий уровень через 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
Пример
Короткая вибрация, как у нативных приложений, для нажатий и результатов. Вызывайте при каждом значимом нажатии.
Возвращает: void (запустил и забыл). kind: 'light' (по умолчанию), 'medium', 'heavy', 'selection', 'success', 'warning', 'error'. Требуется: Вибрация на шаге 4 (Доступ); сборки с ИИ включают её за вас, когда код вызывает 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>
);
}Для вкладок, переключателей и списков выбора используйте 'selection'; для подтверждения удаления используйте 'heavy':
import { haptic } from '@/lib/appmintNative';
function onTabChange(tab: string) { haptic('selection'); setTab(tab); }
function onConfirmDelete() { haptic('heavy'); deleteItem(); }Кнопка «нравится» с лёгким тиком при включении и более сильным при выключении:
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>
);
}Примечания: В приложении используется плагин Capacitor Haptics (impact, notification, selectionChanged). В вебе используется navigator.vibrate, где он есть в браузере, а в остальных случаях ничего не делается; исключений не бросает. Для более длинных ритмов используйте hapticPlay.
appmintNative.hapticPlay appmintNative#
hapticPlay(pattern) — @/lib/appmintNative
Пример
Проигрывает богатый вибропаттерн для особого момента (победа, серия, сердцебиение). Для обычных нажатий используйте haptic().
Возвращает: Promise<boolean>: true, если шаблон воспроизведён, false, если не удалось. Требуется: Вибрация на шаге 4 (Доступ).
Именованные узоры: '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();
}Ваш собственный паттерн: список { at, duration?, intensity?, sharpness? } (мс, мс, 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
]);Сочетайте с haptic(): обычная тактильная отдача для каждого ответа, длинный узор — только для особого момента:
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
}
}Примечания: Приложение использует плагин Capacitor AppwrightHaptics (сила на каждый удар там, где телефон это поддерживает). В вебе работает только тайминг. sharpness принимается, но на Android не имеет эффекта. haptic.play(pattern) — та же функция.
appmintNative.Haptics appmintNative#
Haptics — @/lib/appmintNative
Пример
Объект Haptics в стиле Capacitor: те же имена и параметры, что у @capacitor/haptics, работающий на том же коде, что и haptic().
Возвращает: каждый метод возвращает void (не промис; await на нём безвреден). Требуется: Вибрация на шаге 4 (Доступ); сборки ИИ включают её сами, когда код использует 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Выбор значения с тактильным щелчком при изменении:
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>
);
}Форма, которая подтверждает вибрацией уведомления и добавляет короткую вибрацию, когда поле заполнено неверно:
import { Haptics } from '@/lib/appmintNative';
export function onSubmit(valid: boolean) {
Haptics.notification({ type: valid ? 'SUCCESS' : 'ERROR' });
if (!valid) Haptics.vibrate({ duration: 200 });
}Примечания: selectionStart и selectionChanged дают по одному тику выбора; selectionEnd ничего не делает. Работает в приложении (плагин Capacitor Haptics); в вебе используется navigator.vibrate, если доступен, иначе ничего не происходит.
appmintNative.has appmintNative#
has(cap: Capability): boolean
Пример
Форма «да/нет» для can(): true, если функция на этой платформе действительно что-то сделает.
Возвращает: boolean синхронно — ровно can(cap) !== 'none'. Требуется: ничего.
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>
);
}Закройте доступ ко всей функции:
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;
}Выбирайте подпись по месту запуска: has() решает, рисовать ли элемент управления, can() сообщает, какого он вида:
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';
}Примечания: принимает те же названия возможностей, что и can() (полный список — в той статье). Используйте can(), когда нужно знать, ГДЕ это запущено ('native' или 'web'), например чтобы изменить подпись.
appmintNative.hideKeyboard appmintNative#
hideKeyboard(): void
Пример
Закрывает экранную клавиатуру — например, когда форма отправлена или поверх поля открылась панель.
Возвращает: ничего. Требуется: ничего.
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>
);
}Закройте клавиатуру, когда поверх поля открывается окно, чтобы оно не пряталось за ней:
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),
};
}Закройте её, когда пользователь нажимает в любое место, не являющееся текстовым полем:
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>
);
}Примечания: В приложении просит плагин Capacitor Keyboard скрыть клавиатуру. В вебе снимает фокус с поля, что и закрывает клавиатуру в браузере. Безопасно вызывать, когда клавиатура не открыта.
appmintNative.inAppCampaigns appmintNative#
import { inAppCampaigns } from '@/lib/appmintNative'
Пример
Сообщения внутри приложения, которые эта установка хранит сейчас, в виде {id, name}[]: кампании автора, в аудиторию которых входит этот пользователь.
Возвращает: { id: string; name: string }[] синхронно; [] в вебе и без AppMint Push. Нужно: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
import { inAppCampaigns } from '@/lib/appmintNative';
console.log(inAppCampaigns().map((c) => c.name));Отладочный список на экране администратора:
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>;
}Пропустите свою подсказку, если у автора уже запущено сообщение с таким именем:
import { inAppCampaigns } from '@/lib/appmintNative';
export const hasCampaign = (name: string) => inAppCampaigns().some((c) => c.name === name);appmintNative.installId appmintNative#
installId(): string
Пример
Даёт стабильный случайный ID для этой установки: собственный ID оболочки в приложении для Android, случайный ID, сохранённый в localStorage, в вебе.
Возвращает: string сразу. Требуется: ничего.
Привяжите бесплатный пробный период к устройству.
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;
}Учтите эту установку один раз в своей аналитике.
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');
}Покажите короткий код поддержки, который можно найти позже, с кнопкой копирования.
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>;
}Примечания: Остаётся прежним при обновлениях приложения и меняется при удалении или «очистке данных». Это не аппаратный ID. В вебе выбрасывает ошибку, когда localStorage заблокирован, — намеренно: общий пустой ID перемешал бы пользователей.
appmintNative.isAlarmRinging appmintNative#
isAlarmRinging() — @/lib/appmintNative
Пример
Звонит ли прямо сейчас один из будильников приложения.
Возвращает: boolean синхронно; в вебе всегда false. Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.
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;
}Показывайте кнопки «Стоп» и «Отложить», пока звонит будильник:
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>
);
}Сначала проверьте поддержку, чтобы страница показывала управление будильником только там, где будильник может зазвонить:
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
Пример
Сообщает, выполняется ли код внутри установленного приложения Android (присутствует мост оболочки window.WebToApk).
Возвращает: boolean сразу. false в браузере. Требуется: ничего.
Покажите значок «Приложение для Android» и ссылку на скачивание в браузере.
import { isNativeApp } from '@/lib/appmintNative';
export function appBadgeText(): string {
if (isNativeApp()) return 'Android app';
return 'Get the app on Google Play';
}Помечайте события аналитики тем, откуда они пришли.
import { isNativeApp, platform } from '@/lib/appmintNative';
export function analyticsSource(): string {
return isNativeApp() ? `app-${platform()}` : 'browser'; // 'app-android' or 'browser'
}Скройте баннер «Скачать приложение» внутри приложения.
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>;
}Примечания: platform() даёт тот же ответ строкой ('android' в приложении, 'web' в браузере). Чтобы решить, можно ли показывать функцию, используйте can('<capability>'): он знает, на какой платформе какая функция есть.
appmintNative.keepAlive appmintNative#
import { keepAlive } from '@/lib/appmintNative'
Пример
Держит приложение работающим в фоне с уведомлением службы переднего плана (start, update, stop, isRunning, onState, available).
Возвращает: start / update → Promise<BgResult> ({ ok, reason?, type?, notificationShown? }; reason: 'unsupported' в браузере или на iOS, 'not_enabled', когда в сборке не отмечено «Работать в фоне»). stop → Promise<boolean>. isRunning() → boolean. onState(cb) → функцию отписки; cb получает деталь appmint:background-service. Требуется: Работа в фоне в сборке.
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();Остановите службу и прочитайте, почему start было отклонено:
import { keepAlive } from '@/lib/appmintNative';
export async function toggleTracking(on: boolean) {
if (!on) { await keepAlive.stop(); return; }
const r = await keepAlive.start({ title: 'Tracking', text: 'Running', type: 'dataSync' });
if (!r.ok) {
// 'unsupported' = browser or iOS; 'not_enabled' = the build did not tick Keep running in background
alert(r.reason === 'not_enabled' ? 'Turn on Keep running in background and rebuild.' : 'Not available here.');
}
}Держите кнопку React в согласии со службой и показывайте прогресс в уведомлении:
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>
Пример
Держит экран включённым, пока показывается рецепт, маршрут, QR-код или таймер, и снова позволяет ему засыпать.
Возвращает: Promise<boolean> — true, если запрос вступил в силу, false, если ничто не смогло удержать экран включённым (или браузер отказал). Нужно: ничего.
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 />;
}Держите экран включённым только пока работает таймер, и сообщайте пользователю, если сделать это не удалось.
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]);
}Сначала проверьте возможность и снова включите блокировку, когда вкладка возвращается на передний план (веб снимает её, когда страница скрыта).
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);
};
}Примечания: в приложении используется плагин KeepAwake (флаг окна «не гасить экран»); в вебе — Screen Wake Lock API, который браузеры сами отпускают, когда вкладка скрыта, — вызовите keepAwake(true) снова, когда страница снова видна, если экран должен оставаться включённым. Работает только пока приложение на переднем плане.
appmintNative.Keyboard appmintNative#
Keyboard.hide() / Keyboard.addListener(event, handler): { remove() }
Пример
Экранная клавиатура в форме плагина Keyboard из Capacitor: закройте её и узнавайте, когда меняется закрываемое ею место.
Возвращает: hide() → Promise<void>. addListener(event, handler) → форма Capacitor: Promise дескриптора, у которого есть и сам remove(), поэтому работают await, .then(h => h.remove()) и .remove(). event — это 'keyboardWillShow', 'keyboardDidShow', 'keyboardWillHide' или 'keyboardDidHide'; обработчик получает { keyboardHeight }. Требуется: ничего.
Закройте клавиатуру, когда форма отправлена или поверх поля открылась панель:
import { Keyboard } from '@/lib/appmintNative';
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
await Keyboard.hide();
await save();
}Слушайте (удаляйте дескриптор при размонтировании):
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(); };
}, []);Объедините оба, чтобы при открытии клавиатуры прокручивать поле с фокусом в видимую область, а затем закрывать клавиатуру по Enter:
import { Keyboard } from '@/lib/appmintNative';
import { useEffect } from 'react';
export function useKeyboardScroll() {
useEffect(() => {
const h = Keyboard.addListener('keyboardDidShow', () => {
document.activeElement?.scrollIntoView({ block: 'center', behavior: 'smooth' });
});
const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter') void Keyboard.hide(); };
window.addEventListener('keydown', onKey);
return () => { h.remove(); window.removeEventListener('keydown', onKey); };
}, []);
}Примечания: keyboardHeight — часть клавиатуры, которую странице ещё нужно обойти, а не сырая высота: в приложении Android оболочка изменяет размер страницы под клавиатуру, поэтому там, пока клавиатура открыта, значение равно 0 (событие показа всё равно срабатывает и срабатывает снова при смене числа). События скрытия срабатывают, только когда клавиатура действительно закрылась, поэтому по ним можно следить, «открыта ли клавиатура». События «will» и «did» срабатывают вместе. Для одного лишь отступа проще onKeyboard((px) => …).
appmintNative.launchUrl appmintNative#
launchUrl(): Promise<string>
Пример
Выдаёт ссылку, с которой приложение было запущено или возобновлено — deep link, ярлык значка, нажатие на виджет, уведомление — чтобы приложение могло открыть нужный экран.
Возвращает: Promise<string> — полный URL или '', если приложение открыли обычным способом. В вебе всегда '', где маршрутом уже служит адресная строка. Требуется: ничего для ярлыков и нажатий на виджет; внешним ссылкам нужна опция Глубокие ссылки при сборке.
Выберите маршрут по нему один раз при монтировании приложения:
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]);
}Используйте вместе с onDeepLink() для ссылок, которые приходят, когда приложение уже открыто:
import { onDeepLink } from '@/lib/appmintNative';
const off = onDeepLink((url) => console.log('opened with', url));Считайте пустую строку признаком «открыто обычным способом» и выбирайте стартовый экран по ссылке, только если она есть. У ярлыка вроде appmint-shortcut://<package>/orders маршрут находится в пути.
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); });
}Примечания:
- Значение СОХРАНЯЕТСЯ, а не потребляется: компонент, смонтированный позже, всё равно его увидит. Более новая ссылка заменяет его.
- Читается из оболочки (
WebToApk.getLaunchUrl()); никогда не вызывайте это напрямую в приложении, созданном ИИ. Ярлыки и нажатия на виджет приходят какappmint-shortcut://<package><route>. - Разберите ссылку через
new URL(u)и перейдите по её пути; не загружайте сам URL.
appmintNative.listCallLog appmintNative#
listCallLog(limit = 100, offset = 0, sinceMillis = 0): Promise<CallLogEntry[]>
Пример
Читает журнал вызовов, сначала новые. Только приложение Android.
Возвращает: Promise<CallLogEntry[]> — каждый { number, name, type, date, duration }; type равно incoming, outgoing, missed, voicemail, rejected, blocked или other, date — эпоха в миллисекундах, duration — секунды. Пусто, если «Журнал вызовов» выключен или в разрешении отказано. Нужно: переключатель Журнал вызовов (чтение) на шаге 4 (Доступ) — сборка включает его сама, когда ваш код вызывает это — и разрешение пользователя при первом использовании.
Пропущенные звонки за последние 7 дней.
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
}
}Общее время разговоров по каждому контакту по исходящим и входящим звонкам, сначала самые долгие.
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]);
}Экран, объясняющий пустой список, поскольку «нет звонков» и «в разрешении отказано» выглядят одинаково.
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 };
}Примечания: limit ограничивается приложением до 2000. По пустому списку нельзя отличить «нет звонков» от «разрешение отклонено». can('callLog') в вебе равно 'none'. Журнал звонков — ограниченное разрешение Google Play: вам нужна одобренная декларация разрешений в Play Console, иначе приложение отклонят.
appmintNative.listContacts appmintNative#
listContacts(limit = 200, offset = 0): Promise<Contact[]>
Пример
Читает адресную книгу постранично. Только в приложении для Android, через мост оболочки.
Возвращает: Promise<Contact[]>. Пустой массив, если контактов нет, доступ отклонён или Контакты не были включены при сборке. Требуется: переключатель Контакты на шаге 4 (Доступ) — сборка включает его, когда ваш код вызывает это, — и разрешение пользователя при первом использовании.
Экран списка контактов. Показывайте его только там, где он может работать. Каждый контакт — { 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
}
}Поиск по всей адресной книге — читая её постранично в один список.
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));
}Сочетайте с sendSms, чтобы написать на первый номер контакта.
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!`);
}Примечания: по пустому результату нельзя отличить «нет контактов» от «разрешение отклонено» — когда список пуст, покажите короткую подсказку («Разрешите доступ к контактам в настройках»). Контакты приходят отсортированными по имени от А до Я; limit ограничивается 2000. Для одного человека используйте pickContact(), которому разрешение не нужно. Контакты — личные данные: укажите их в форме безопасности данных Play и в политике конфиденциальности.
appmintNative.listSms appmintNative#
listSms(box: 'inbox' | 'sent' | 'draft' = 'inbox', limit = 100, offset = 0): Promise<SmsMessage[]>
Пример
Читает SMS из входящих, отправленных или черновиков, сначала новые. Только приложение Android.
Возвращает: Promise<SmsMessage[]> — каждое { id, address, body, date, read, box }. Пусто, когда SMS выключены, разрешение отклонено или ящик пуст. Требуется: переключатель SMS (чтение и отправка) на шаге 4 (Доступ) — сборка включает его, когда ваш код вызывает это, — и разрешение пользователя при первом использовании.
Покажите последние 20 сообщений из входящих.
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
}
}Непрочитанные сообщения от одного отправителя, сначала новые.
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);
}Прочитайте отправленные и черновики за один раз и отсортируйте их вместе по дате.
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));
}Примечания: По пустому списку нельзя отличить «нет сообщений» от «в разрешении отказано». limit ограничивается приложением до 500. can('smsList') в вебе равно 'none'. Новые входящие сообщения в эту обёртку не входят; страницы, написанные вручную, получают их из события appmint:sms-received (см. listSms для прямого моста). SMS — ограниченное разрешение Google Play: нужна одобренная декларация разрешений в Play Console, которую обычно выдают только приложениям SMS по умолчанию.
appmintNative.liveProgress appmintNative#
import { liveProgress } from '@/lib/appmintNative'
Пример
Закреплённая беззвучная карточка статуса в шторке уведомлений Android, которую вы обновляете на месте, — доставка, загрузка, таймер, тренировка. start, update, end.
Возвращает: start / update → Promise<boolean> (false, когда недоступно, уведомления не разрешены или пользователь отключил его категорию «Живой статус»); end → Promise<void>. Требуется: приложение для Android и разрешение на уведомления на Android 13+. В вебе can('liveProgress') равно 'none' — показывайте то же состояние внутри приложения.
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
}Работающий таймер: startedAt показывает часы, отсчитывающие время с этого момента.
await liveProgress.start({ id: 'run', title: 'Morning run', text: '5 km goal', startedAt: Date.now() });
// ...later
await liveProgress.end('run');Обработайте отказ. start отвечает false; liveProgress.lastError сообщает причину.
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;
}Примечания: один и тот же id обновляет одну карточку; разные id создают отдельные карточки (id по умолчанию 'live'). Пользователь не может её смахнуть, поэтому всегда вызывайте end. На Android 13+ сначала запросите разрешение на уведомления (Notification.requestPermission()): без него start отвечает false и ничего не показывается.
appmintNative.LocalNotifications appmintNative#
import { LocalNotifications } from '@/lib/appmintNative'
Пример
Форма @capacitor/local-notifications (LocalNotifications.schedule({ notifications })) для кода, написанного под плагин Capacitor. Уведомление без schedule показывается сразу (через notify()); уведомление с schedule: { at } — напоминание на это время, сохраняемое после перезапусков.
Возвращает: Promise<{ notifications: { id }[] }>. Напоминание, которое нельзя установить (браузер, сборка без уведомлений, отказ в разрешении), отклоняется с причиной — вместо этого оно никогда не показывается раньше времени. Повторяющиеся напоминания: scheduleAlarm или API напоминаний. Нужно: Android: включённые Уведомления на шаге «Доступ» (шаг 4) при сборке.
import { LocalNotifications } from '@/lib/appmintNative';
export async function announceSync(count: number) {
await LocalNotifications.schedule({
notifications: [
{ title: 'Sync complete', body: `${count} items updated`, tag: 'sync' },
],
});
}Несколько уведомлений в одном вызове, каждое заменяет более раннее с тем же 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' }] : []),
],
});
}Когда нужно знать, было ли показано, используйте вместо этого notify() и реагируйте на ответ.
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
}Примечания: только немедленные — поля Capacitor id, schedule, at, actionTypeId и extra не поддерживаются; каждый элемент — { title, body?, icon?, tag? }. Для уведомления в более позднее время страница, написанная вручную, использует WebToApk.scheduleNotificationEx (Android). Для кнопок используйте notifyWithActions.
appmintNative.lockOrientation appmintNative#
lockOrientation(to: 'portrait' | 'landscape' | 'any'): Promise<boolean>
Пример
Фиксирует экран в одной ориентации для представления, которое имеет смысл только так, — игровое поле, широкий график, поле для подписи. 'any' снимает блокировку.
Возвращает: Promise<boolean> — true, если блокировка (или снятие) применена, false, если платформа отказала. Никогда не отклоняется. Нужно: ничего.
Блокируйте, пока экран открыт, и снимайте блокировку при уходе:
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>;
}Проверьте ответ, если от него зависит экран:
import { lockOrientation, toast } from '@/lib/appmintNative';
const ok = await lockOrientation('portrait');
if (!ok) toast('Turn your phone upright for the best view');Снимайте блокировку, когда экран исчезает, — как хук, общий для нескольких экранов.
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Примечания:
- В приложении блокировку применяет плагин Capacitor ScreenOrientation.
- В вебе используется
screen.orientation.lock(). Большинство браузеров отказывают вне полноэкранного режима, а настольные браузеры отказывают всегда, поэтому там ожидайтеfalse. - Всегда оставляйте пользователю выход из заблокированного экрана.
appmintNative.longPress appmintNative#
longPress(el: HTMLElement, handler: (e: PointerEvent) => void, opts?: { ms?: number; haptics?: boolean }): () => void
Пример
Вызывает вас, когда пользователь удерживает палец на элементе, — для контекстного меню строки, реакций или действий карточки.
Возвращает: функцию отписки — вызовите её при размонтировании. Требуется: ничего.
Параметры: ms — время удержания, по умолчанию 450; haptics — средняя тактильная отдача при срабатывании, по умолчанию true.
Открывайте окно действий по долгому нажатию, с видимой кнопкой для того же меню:
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>
);
}Более медленное удержание без тактильного отклика:
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 });Сочетайте с haptic для своего подтверждения и выполняйте очистку для каждой строки списка.
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>;
}Примечания:
- Срабатывает один раз за удержание и поглощает клик, который последовал бы за ним, поэтому долгое нажатие не считается ещё и «нажатием».
- Сдвиг больше чем на 8 px отменяет жест, поэтому прокрутка его никогда не вызывает. Он также блокирует собственное меню браузера по долгому нажатию на этом элементе.
- Долгое нажатие не видно. Всегда оставляйте видимую кнопку для того же действия.
appmintNative.markPushRead appmintNative#
import { markPushRead } from '@/lib/appmintNative'
Пример
Отмечает одно входящее сообщение AppMint Push как прочитанное или все сообщения, если вызвать без аргумента.
Возвращает: boolean синхронно — true, если что-то изменилось. В вебе всегда false. Требуется: Android: AppMint Push, включённый на шаге «Интеграция» (шаг 3) при сборке.
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();
}Посчитайте непрочитанные сообщения после отметки одного, для значка в вашем собственном заголовке.
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 };
}Отметьте всё прочитанным, когда пользователь открывает экран входящих, и обновляйте при приходе нового сообщения.
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;
}Примечания: состояние прочтения хранится только на этом телефоне; уведомление в шторке не удаляется.
appmintNative.mediaSession appmintNative#
mediaSession — @/lib/appmintNative
Пример
Управление воспроизведением на экране блокировки и в уведомлении для приложения, которое воспроизводит аудио или видео: публикуйте, что играет, получайте кнопки «Воспроизвести» / «Пауза» / «Далее» / «Назад» / «Стоп» и передавайте видео на телевизор.
Возвращает: setMetadata, setPlaybackState, clear → void. onAction(handler) → функция отписки. cast(url) → Promise<boolean> (false, когда ничего нельзя было предложить). Требуется: Медиауведомление на шаге 4 (Доступ) при сборке; сборки ИИ включают его сами, когда код использует 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} />;
}Отправьте текущее видео на телевизор:
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>;
}Очистите карточку на экране блокировки, когда воспроизведение закончилось, и не трогайте её, если на платформе нет элементов управления медиа.
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);
}Примечания:
- На Android кнопки приходят через
navigator.mediaSessionприложения, который устанавливается, когда страница заканчивает загрузку.onAction, зарегистрированный раньше (например, в эффекте монтирования), теряется, поэтому регистрируйте его при первом воспроизведении. - Действие
seek(сpositionMs) доставляется только в вебе; в уведомлении Android перемотки нет. artworkUrlдолжен быть полным адресомhttps://в интернете. На Androidcast(url)открывает окно выбора «Трансляция на…» (передача; завершаетсяtrue, как только окно запрошено). В вебе используется Remote Playback API.
appmintNative.Motion appmintNative#
Motion.addListener('accel' | 'orientation', cb): Promise<{ remove() }>
Пример
Форма @capacitor/motion поверх стандартных событий devicemotion и deviceorientation: движение и наклон телефона. Один и тот же код работает в приложении и в браузере.
Возвращает: Promise<{ remove(): Promise<void> }> — вызовите remove(), чтобы остановить. 'accel' вызывает обратно с { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }; 'orientation' — с { alpha, beta, gamma } (градусы). Нужно: ничего.
Встряхните для обновления.
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;
}Наклоняйте пузырёк уровня по событию ориентации.
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();Форма хука с автоматической очисткой для компонента React, который читает движение телефона.
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 };
}Примечания: Некоторые браузеры сначала запрашивают доступ к датчикам движения, поэтому там запускайте по нажатию; если пользователь отказал, дескриптор возвращается, но события не приходят. can('motion') равно 'none', где в браузере нет DeviceMotionEvent. Отсутствующие значения датчиков приходят как 0. Всегда вызывайте remove(), когда экран закрывается — события движения срабатывают много раз в секунду.
appmintNative.musicVolume appmintNative#
musicVolume() — @/lib/appmintNative
Пример
Читает уровень зацикленной фоновой музыки приложения, от 0 до 100.
Возвращает: number синхронно: 0–100 или -1, если музыки нет (в вебе или в приложении, собранном без музыкального файла). Требуется: файл, выбранный в разделе Фоновая музыка на шаге 3 (Интеграция) при сборке.
Показывайте ползунок музыки, только когда есть музыка:
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>
);
}Держите уровень в согласии с переключателем «Выключить музыку», запоминая громкость, к которой нужно вернуться.
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>;
}Приглушите музыку на время воспроизведения видео, затем восстановите её.
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));
}Примечания: только в приложении для Android; в вебе отвечает -1. Уровень, заданный через setMusicVolume, сохраняется до следующего запуска; пока страница не задала его, это уровень сборки.
appmintNative.Network appmintNative#
Network.getStatus(): Promise<{ connected, connectionType }> / Network.addListener(event, handler): { remove() }
Пример
Состояние онлайн/офлайн в форме плагина Network из Capacitor — тот же код, что networkStatus() / onNetworkChange().
Возвращает: getStatus() → Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) → форма Capacitor: Promise дескриптора, у которого есть и сам remove(); обработчик получает одну и ту же форму при каждом изменении. Требуется: ничего.
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;
}Прочитайте текущий тип один раз и покажите его в чипе состояния.
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>;
}Задержите большую загрузку при мобильных данных и обработайте также случай без сети.
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();
}Примечания: Имя события игнорируется — каждый слушатель получает изменения подключения. connectionType — это 'wifi', 'cellular', 'none' или 'unknown': в приложении getStatus() и слушатель используют плагин Network, в вебе — navigator.onLine / navigator.connection.type. connected означает, что у телефона есть сеть, а не что ваш сервер отвечает.
appmintNative.networkStatus appmintNative#
networkStatus(): { connected: boolean; type: string }
Пример
Прямо сейчас говорит, онлайн ли устройство.
Возвращает: { connected, type } синхронно. type — это 'wifi', 'cellular', 'none' (офлайн) или 'unknown'. Требуется: ничего.
Достаточно дёшево, чтобы вызывать во время отрисовки:
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>
);
}Прочитайте начальное значение, затем следите за изменениями:
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;
}Выберите меньший размер изображений в мобильном интернете, ответив и на случай 'none' (офлайн).
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'}`;
}Примечания:
- В приложении это ответ плагина Network (поддерживается актуальным в фоне, поэтому самый первый вызов ещё может вернуть значение самого WebView). В вебе это
navigator.onLineплюсnavigator.connection.type, если браузер его сообщает, иначе'unknown'— класс скорости вроде'4g'никогда не возвращается как тип. - «Подключено» означает, что у устройства есть сеть, а не что ваш сервер отвечает. Всё равно обрабатывайте неудавшиеся запросы.
appmintNative.nfc appmintNative#
import { nfc } from '@/lib/appmintNative'
Пример
Вспомогательные функции для Web NFC в один вызов (NDEFReader): проверить поддержку, читать метки, записать текстовую метку. Приложение для Android использует свой NDEFReader; в вебе это собственный Web NFC в Chrome (Chrome на Android).
Возвращает: isSupported(): Promise<boolean>; scan(onTag): Promise<() => Promise<void>> (функция остановки), а onTag получает Array<{recordType, text, mediaType?}> на каждую метку — названия типов Web NFC ('text', 'url', 'mime', …) и декодированный текст на всех платформах; write(text): Promise<boolean> (true после записи метки, false при любом сбое). Требуется: на Android включите Поддержку Web NFC на шаге 4 (Доступ) при сборке. scan() выбрасывает ошибку, если нет NDEFReader — сначала проверьте 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>
);
}Запишите ссылку в метку и сообщите пользователю, почему не получилось, если так случилось.
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.');
}Прочитайте один тег, затем остановите сканирование.
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);
});
}Примечания: на Android isSupported() равно true, когда в сборке есть NFC — это не говорит, включён ли NFC (это говорит window.AndroidNFC.startScan()). После функции остановки onTag больше не вызывается. На Android само приложение продолжает ловить метки, пока оно на переднем плане (чтобы касание не открывало другое приложение), просто перестаёт передавать их вам. text текстовой записи не имеет языкового префикса; text записи-URL — полный URL. write() ждёт, пока приложат метку. Для записи URL или mime-записей и для блокировки метки используйте NDEFReader напрямую.
appmintNative.notify appmintNative#
import { notify } from '@/lib/appmintNative'
Пример
Показывает одно уведомление прямо сейчас из приложения, созданного с помощью ИИ, — в приложении для Android и в вебе.
Возвращает: Promise<boolean> — true, если отправлено, false, если пользователь отказал в разрешении на уведомления, уведомления для приложения выключены или здесь недоступны. Само запрашивает разрешение в первый раз (Android 13+ и веб). Нужно: Android: включённые Уведомления на шаге «Доступ» (шаг 4) при сборке.
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.');
}Спрашивайте в удачный момент: первый notify() показывает диалог разрешения. Чтобы спросить со своей кнопки «Включить оповещения» — до того, как что-либо опубликовано, — используйте стандартный API, который приложение тоже поддерживает:
async function enableAlerts() {
if (typeof Notification === 'undefined') return false;
const perm = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
return perm === 'granted';
}Откройте нужный экран, когда пользователь нажимает на уведомление, сочетая notify с 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;
}Примечания: Android использует мост уведомлений оболочки (icon становится большим значком; используйте изображение https:// или data:); веб использует Notification API. Если пользователь отказал окончательно, notify() завершается false без повторного запроса — направьте его в настройки телефона. Для кнопок используйте notifyWithActions; для закреплённой карточки прогресса используйте liveProgress.
appmintNative.notifyWithActions appmintNative#
import { notifyWithActions } from '@/lib/appmintNative'
Пример
Показывает уведомление не более чем с 3 кнопками («Готово», «Отложить»). Нажатие возвращается через onNotificationAction.
Возвращает: Promise<boolean> — true, если отправлено, false, если в разрешении на уведомления отказано (первый раз оно запрашивается, как у notify()). Нужно: Android: включённые Уведомления на шаге «Доступ» (шаг 4) при сборке и разрешение пользователя на Android 13+. В вебе can('notificationActions') равно 'none'.
import { notifyWithActions, onNotificationAction, can } from '@/lib/appmintNative';
export async function remindTask(taskId: string, title: string) {
if (can('notificationActions') === 'none') return false; // web: hide the buttons
return notifyWithActions({
title,
body: 'Due now',
tag: `task-${taskId}`, // comes back as e.tag
actions: [
{ id: 'done', title: 'Mark done' },
{ id: 'snooze', title: 'Snooze' },
],
});
}
// Mount once (e.g. in App.tsx):
export const stopListening = onNotificationAction((e) => {
// e = { actionId: 'snooze', tag: 'task-42', text: '' }
const taskId = e.tag.replace('task-', '');
if (e.actionId === 'done') completeTask(taskId);
if (e.actionId === 'snooze') snoozeTask(taskId);
});Таймер закончился: одна кнопка добавляет пять минут, другая останавливает его.
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);
});Обработайте отказ в разрешении после вызова и наводите порядок в компоненте.
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>;
}Примечания: Каждая кнопка показывает свой title; её id возвращается как e.actionId. Опция input (встроенное поле ответа) в этом приложении не поддерживается — она игнорируется, у кнопки нет поля ответа, а e.text всегда равно ''. Нажатие доходит до страницы, только пока приложение работает. В вебе обёртка отправила бы обычное уведомление без кнопок, поэтому в примере сначала проверяется can().
appmintNative.offlineBanner appmintNative#
offlineBanner(opts?: { message?: string; onlineMessage?: string }): () => void
Пример
Монтирует готовую полосу, которая выезжает под строкой состояния, когда соединение пропадает, и ненадолго показывает «Снова в сети», когда оно возвращается.
Возвращает: функцию отписки, которая убирает баннер и прекращает прослушивание. Нужно: ничего.
Смонтируйте один раз при запуске приложения:
import { useEffect } from 'react';
import { offlineBanner } from '@/lib/appmintNative';
export function App() {
useEffect(() => offlineBanner(), []); // cleanup removes it
return <main>{/* … */}</main>;
}Ваши собственные слова (например, на языке приложения):
import { offlineBanner } from '@/lib/appmintNative';
const off = offlineBanner({ message: 'Sin conexión', onlineMessage: 'Conectado de nuevo' });Монтируйте баннер только для области приложения после входа и убирайте его при выходе.
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]);
}Примечания:
- По умолчанию: «Вы не в сети» и «Снова в сети». Сообщение о появлении сети скрывается примерно через 1,8 секунды; сообщение об отсутствии остаётся, пока связь не вернётся.
- Если устройство уже офлайн при монтировании, баннер показывается сразу.
- Построен на
onNetworkChange(), поэтому работает на любой поверхности. Цвета берутся из токенов приложения (--ink/--bgофлайн,--accent/--accent-inkонлайн). У него естьrole="status"для программ чтения с экрана. - Монтируйте один раз. Два вызова рисуют два баннера.
appmintNative.onAlarm appmintNative#
onAlarm(cb) — @/lib/appmintNative
Пример
Слушает исходы будильника: приложение, открытое будильником (action: 'open', с ringing), и то, как закончился звонок, — dismiss, snooze, missed (прозвонил до конца или заменён другим будильником) или error. Исходы, случившиеся при закрытом приложении, доставляются при следующей загрузке страницы.
Возвращает: функцию, прекращающую прослушивание. Функция получает { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.
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;
}Поставьте будильник, сначала получив ответ 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.';
}Остановите или отложите звонящий будильник со своего экрана и отреагируйте на результат.
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>
);
}Примечания: обычные напоминания, открывающие приложение, тоже приходят сюда с action: 'open' и ringing: false.
appmintNative.onBackGesture appmintNative#
onBackGesture(handler: () => boolean): () => void
Пример
Позволяет открытой панели, модальному окну, лайтбоксу или шагу мастера первыми ответить на жест НАЗАД — свайп от края / кнопка «назад» в приложении Android, кнопка «назад» браузера в вебе.
Возвращает: функцию отписки — вызовите её, когда экран закроется или будет размонтирован. Ваш обработчик возвращает true, чтобы поглотить нажатие (вы что-то закрыли), или false, чтобы пропустить его. Нужно: ничего.
Регистрируйте только пока панель открыта:
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>}
</>
);
}Шаг назад по мастеру, а «Назад» на последнем шаге пусть работает как обычно:
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]);
}Закрывайте просмотр изображения жестом «назад», привязав регистрацию к тому, открыто ли изображение.
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;
}Примечания:
- Обработчики образуют стек: последний зарегистрированный (окно сверху) спрашивается первым. Обработчик, выбросивший ошибку, ничего не поглощает.
- На Android это идёт через обработчик «назад» оболочки. Без него свайп назад при открытой панели закрывает всё приложение.
- В вебе добавляет одну запись в историю, пока зарегистрирован хотя бы один обработчик, и убирает её, когда отписывается последний.
- Всё равно рисуйте видимую кнопку «назад» / «закрыть»: многие пользователи никогда не пользуются жестом «назад».
- React Router уже обрабатывает обычный переход назад по страницам; используйте это только для состояния, о котором роутер не знает. Всегда отписывайтесь, иначе закрытая панель продолжит перехватывать нажатия «Назад».
appmintNative.onDeepLink appmintNative#
onDeepLink(cb: (url: string) => void): () => void
Пример
Вызывает вас, когда ссылка открывает уже запущенное приложение — глубокая ссылка, ярлык значка, нажатие виджета.
Возвращает: функцию отписки — вызовите её при размонтировании. Нужно: для ярлыков и нажатий на виджет ничего; для внешних ссылок при сборке нужна опция Deep Linking.
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { launchUrl, onDeepLink } from '@/lib/appmintNative';
export function DeepLinkRouter() {
const navigate = useNavigate();
useEffect(() => {
const go = (u: string) => { const { pathname, search } = new URL(u); navigate(pathname + search); };
void launchUrl().then((u) => { if (u) go(u); }); // the link that started the app
return onDeepLink(go); // links while it runs; returns the unsubscribe
}, [navigate]);
return null;
}Страница, написанная вручную (без React), может слушать то же событие оболочки напрямую:
window.addEventListener('appmint:deep-link', function (e) {
var url = e.detail && e.detail.url;
if (url) console.log('deep link', url);
});Открывайте конкретный товар при нажатии на виджет или ярлык, игнорируя ссылки, которые не наши.
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { onDeepLink } from '@/lib/appmintNative';
export function ProductLinks() {
const navigate = useNavigate();
useEffect(() => onDeepLink((url) => {
const m = new URL(url).pathname.match(/^\/product\/(\w+)$/);
if (m) navigate(`/products/${m[1]}`);
}), [navigate]);
return null;
}Примечания:
- Android: слушает событие оболочки
appmint:deep-link(detail.url); оболочка также вызываетwindow.onAppMintDeepLink(detail), когда страница его определяет. После прихода ссылки её возвращает иlaunchUrl(). - Веб: никогда не срабатывает (обратный вызов не вызывается), а отписка ничего не делает.
appmintNative.onInAppMessage appmintNative#
import { onInAppMessage } from '@/lib/appmintNative'
Пример
Вызывает вас для каждого сообщения в приложении (кампании автора), которое показано, нажато, закрыто или на которое дан ответ на этой установке. Возвращает функцию отписки.
Возвращает: () => void. Обратный вызов получает {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. В вебе никогда не срабатывает. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
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]);
}Рисуйте сообщения сами (с setInAppPresentation('none') presented равно false, а message содержит оформление):
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) };
}Реагируйте на кнопку, которую пользователь нажал в собственной отрисовке приложения:
import { onInAppMessage } from '@/lib/appmintNative';
onInAppMessage((e) => {
if (e.type === 'clicked' && e.link === 'appmint://notifications/enable') refreshNotificationBadge();
});Примечания: button равно -1 при нажатии на само сообщение, иначе индекс кнопки.
Ответы (type: 'answered'). Сообщение со строкой звёзд (Опрос со звёздами, Оцените нас) или с баллами 0–10 (Оценка NPS) сообщает ответ один раз, при закрытии: question — это 'stars' (значение 1–5) или 'nps' (значение 0–10), а template называет шаблон из галереи, из которого он взят. Центр отправки показывает среднюю оценку или NPS; ваш код может действовать дальше:
import { onInAppMessage } from '@/lib/appmintNative';
onInAppMessage((e) => {
if (e.type === 'answered' && e.question === 'nps' && (e.value ?? 10) <= 6) openFeedbackSheet();
});Ведите собственный учёт оценок в звёздах:
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 }) });
});Поблагодарите тех, кто поставил высокую оценку, в компоненте 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
Пример
Сообщает, сколько пикселей страницы закрывает экранная клавиатура, при каждом изменении, чтобы нижняя панель могла подниматься над ней.
Возвращает: функцию отписки — вызовите её при размонтировании. Функция получает число в CSS-пикселях: нужный дополнительный нижний отступ, 0, когда клавиатура закрыта. Требуется: ничего.
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>
);
}Держите список чата прокрученным к новейшему сообщению, пока открывается клавиатура:
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>;
}Переиспользуемый хук с отступом безопасной зоны, который исчезает, пока открыта клавиатура:
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>Примечания:
- В приложении слушает плагин Capacitor Keyboard и сообщает только ту часть клавиатуры, которую вёрстка ещё не учла. Оболочка обычно изменяет размер страницы под клавиатуру, поэтому значение там часто равно
0— это правильно; не добавляйте сырую высоту клавиатуры сами. - В полноэкранном приложении или приложении-киоске (страница не меняет размер) сообщает полную высоту клавиатуры.
- В вебе использует
visualViewport, один раз сразу вызывает обратный вызов и считает значения меньше 80 px интерфейсом браузера (сообщается как0). - В приложении первый вызов приходит, когда клавиатура открывается впервые, поэтому начните своё состояние с
0.
appmintNative.onNetworkChange appmintNative#
onNetworkChange(cb: (status: { connected: boolean; type: string }) => void): () => void
Пример
Вызывает вас всякий раз, когда соединение пропадает или восстанавливается — для офлайн-баннера или для повтора записей в очереди.
Возвращает: функцию отписки — вызовите её при размонтировании. Требуется: ничего.
Повторяйте поставленную в очередь работу, когда устройство снова в сети:
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]);
}Сразу покажите текущее состояние, затем следите за изменениями (при подписке обратный вызов не вызывается):
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;
}Откладывайте большие загрузки в мобильном интернете и считайте 'none' офлайном:
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]);
}Примечания:
- Он НЕ вызывает вас с текущим состоянием при подписке. Прочитайте
networkStatus()для начального значения. - В приложении слушает плагин Capacitor Network (
networkStatusChange);type— это'wifi','cellular','none'или'unknown'. - В вебе слушает события
online/offlineи сообщаетnetworkStatus(). - Для готового баннера используйте
offlineBanner(), который построен на этом.
appmintNative.onNotificationAction appmintNative#
import { onNotificationAction } from '@/lib/appmintNative'
Пример
Слушает нажатия на кнопки уведомлений (из notifyWithActions).
Возвращает: функцию, прекращающую прослушивание. Функция получает { actionId, tag } — идентификатор кнопки и тег уведомления. Приложение для Android не присылает text ответа (встроенный ответ не поддерживается), поэтому не полагайтесь на e.text. Требуется: то же, что 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]);
}Необработанное событие на странице, написанной вручную (приложение для Android):
window.addEventListener('appmint:notification-action', function (e) {
// e.detail = { actionId: 'Mute', tag: 'chat-7' }
console.log(e.detail.actionId, e.detail.tag);
});Отправляйте уведомление с кнопками только там, где сборка их поддерживает, и обрабатывайте нажатие по тегу уведомления:
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]);
}Примечания: в приложении для Android это событие appmint:notification-action (также window.onAppMintNotificationAction(detail)); в вебе ничего не делает. Нажатие доставляется, только пока приложение работает — нажатие кнопки не открывает приложение.
appmintNative.onNotificationClick appmintNative#
import { onNotificationClick } from '@/lib/appmintNative'
Пример
Запускает функцию, когда пользователь нажимает на уведомление, отправленное приложением, включая нажатие, запустившее приложение с нуля, чтобы приложение могло открыть нужный экран.
Возвращает: функцию отписки. Обратный вызов получает { tag, title, body, data } — data это то, что вы передали в notify({ …, data }). Нужно: уведомления, как у notify(). Событие за этим: 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 } });
}Откройте нужный экран после нажатия, даже если оно запустило приложение с нуля, и игнорируйте уведомления без данных:
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]);
}Отправляйте уведомление только там, где уведомления существуют, с данными для нажатия (data нужны 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');
}Примечания: Нажатие проходит через заставку и экран PIN на страницу. data должно быть JSON (до 16 КБ) и требует tag.
appmintNative.onPullToRefresh appmintNative#
onPullToRefresh(el: HTMLElement, onRefresh: () => void | Promise<void>, opts?: { threshold?: number }): () => void
Пример
Добавляет жест «потяни вниз, чтобы обновить» к прокручиваемому списку: потяните вниз вверху, отпустите — и данные перезагрузятся.
Возвращает: функцию отписки — вызовите её при размонтировании. Требуется: ничего.
el — элемент, который прокручивается (или document.documentElement, если прокручивается вся страница). Индикатор остаётся, пока не завершится promise из onRefresh:
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>;
}Страница, прокручиваемая целиком, с более длинным потягиванием:
import { onPullToRefresh } from '@/lib/appmintNative';
declare function reloadFeed(): Promise<void>;
const off = onPullToRefresh(document.documentElement, reloadFeed, { threshold: 96 });Обновите список через хук данных с тактильным откликом, когда данные пришли:
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">
}Примечания:
- Запускается, только когда список прокручен в самый верх. По умолчанию
threshold— 72 px оттягивания. - Рисует круглый индикатор в цветах приложения (
--surface,--accent), даёт средний тактильный отклик в момент срабатывания и игнорирует новые протягивания, пока идёт обновление. - Он обновляет ваши ДАННЫЕ, а не страницу. Собственное обновление страницы потягиванием в оболочке (Включить обновление потягиванием на шаге 3 (Интеграция)) выключено по умолчанию — оставьте его выключенным, когда используете это, чтобы потягивание не перезагружало ещё и страницу.
- Построено на Pointer Events, поэтому одинаково на всех платформах. Устанавливает
overscroll-behavior-y: containнаelи восстанавливает при отписке.
appmintNative.onVolumeChange appmintNative#
onVolumeChange(cb) — @/lib/appmintNative
Пример
Вызывает cb всякий раз, когда меняется один из уровней громкости телефона — аппаратные кнопки, системный ползунок, другое приложение.
Возвращает: функцию, которая прекращает прослушивание (последняя остановившаяся выключает нативное наблюдение). Обратный вызов получает { stream, current, max, percent, muted? } для каждого изменившегося потока. В вебе ничего не делает. Нужно: ничего.
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>;
}Предупредите, если громкость медиа убрана до нуля во время воспроизведения видео, и прекратите прослушивание при размонтировании:
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]);
}Отразите уровень телефона на ползунке и запишите изменение пользователя обратно (в вебе громкости нет, поэтому getVolume там равно null):
import { useEffect, useState } from 'react';
import { getVolume, setVolume, onVolumeChange } from '@/lib/appmintNative';
export function MediaVolumeSlider() {
const [pct, setPct] = useState<number | null>(() => getVolume('media')?.percent ?? null);
useEffect(() => onVolumeChange((v) => { if (v.stream === 'media') setPct(v.percent); }), []);
if (pct === null) return null; // not the Android app
return (
<input type="range" min={0} max={100} value={pct}
onChange={(e) => { const r = setVolume('media', Number(e.target.value)); if (r.ok) setPct(Number(e.target.value)); }} />
);
}appmintNative.openNotificationSettings appmintNative#
openNotificationSettings(): Promise<boolean>
Пример
Открывает экран настроек уведомлений самого приложения (приложение Android).
Возвращает: Promise<boolean> — true, если экран открылся; false в вебе и везде, где такого экрана нет. Проверьте can('notificationSettings') перед отрисовкой элемента управления. Нужно: ничего.
import { can, openNotificationSettings } from '@/lib/appmintNative';
export function NotificationSettingsRow(row: HTMLElement) {
row.hidden = can('notificationSettings') === 'none';
row.addEventListener('click', () => { void openNotificationSettings(); });
}Примечания: для включения уведомлений одним нажатием используйте enableNotifications(), который открывает этот экран, только когда диалог разрешения больше нельзя показать.
appmintNative.passkeys appmintNative#
passkeys — available / create / get / origin / assetLinks
Пример
Вход и регистрация по ключу доступа (passkey) с JSON-параметрами, которые создаёт ваш passkey-сервер. В приложении Android используется менеджер passkey Android; в вебе — navigator.credentials.
Возвращает: available() → 'native' | 'web' | 'none'; create(optionsJSON) → промис RegistrationResponseJSON; get(optionsJSON) → промис AuthenticationResponseJSON (отправьте любой из них обратно на ваш сервер POST-запросом). Они отклоняются с DOMException (NotAllowedError, InvalidStateError, SecurityError, NotSupportedError). origin() → строка, assetLinks() → массив (только в приложении; '' / [] в вебе). Требуется: сервер passkey; в приложении также Android 9+ с сервисами Google Play и 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),
});
}Значения одноразовой настройки из приложения, установленного через 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Предлагайте кнопку passkey только там, где она может работать, и сообщайте, когда не может:
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>;
}Примечания: в вебе нужна защищённая страница (https://). У страницы, встроенной в приложение, нет домена: параметры вашего сервера должны задавать rp.id / rpId равным вашему домену. Автозаполнение passkey (mediation: 'conditional') в приложении недоступно — начинайте с кнопки.
appmintNative.paste appmintNative#
paste(): Promise<string>
Пример
Читает текст из системного буфера обмена.
Возвращает: Promise<string> — текст из буфера обмена или '', если буфер пуст. ОТКЛОНЯЕТСЯ, если этот браузер вообще не может читать буфер обмена или пользователь отказал в разрешении. Требуется: ничего.
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>
);
}Форма в стиле Capacitor:
import { Clipboard } from '@/lib/appmintNative';
const { value } = await Clipboard.read();Вставляйте в текстовое поле с сообщением для каждого исхода и сочетайте с 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');
}Примечания: В приложении текст читает плагин Clipboard; пустой буфер даёт '', а не ошибку. В браузере это navigator.clipboard.readText() (только Chromium), который запрашивает у пользователя разрешение — вызывайте по нажатию.
appmintNative.pickContact appmintNative#
pickContact(): Promise<Contact | null>
Пример
Открывает системное средство выбора контакта, чтобы пользователь выбрал одного человека: средство выбора Android в приложении, Contact Picker API в Chrome на Android.
Возвращает: Promise<Contact | null> — null, когда пользователь отменяет или доступ отклонён. Требуется: ничего (системному выбору разрешение не нужно).
Выберите человека и заполните форму. У контакта одинаковая структура в приложении и в вебе: { 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
}
}Кнопка «Выбрать контакт», показывающая выбор, и обычное поле для номера там, где средства выбора нет:
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>
);
}Передайте выбранный номер приложению для сообщений, показав сообщение, если у контакта номера нет:
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)}`;
}Примечания: В приложении возвращается только выбранный номер, никогда не адреса почты, а photoUri — адрес content:// фото контакта. can('contactPick') равно 'none' в браузерах без Contact Picker API (настольные, Safari) — не рисуйте там кнопку.
appmintNative.pickFile appmintNative#
pickFile(accept?) from '@/lib/appmintNative'
Пример
Позволяет пользователю выбрать один файл и прочитать его как текст. В приложении открывается средство выбора документов Android; в вебе — обычное поле выбора файла.
Возвращает: Promise<PickedFile | null> — { name, mime, size, text(): Promise<string> }, или null только когда пользователь отменяет. Отклоняется с Error, у которого code равно 'no-picker' (на телефоне нет приложения для выбора документов), 'disabled' или 'failed', когда выбор не удалось открыть. text() читает файл целиком и отклоняется с кодом моста (например 'not-found'), когда не может. Требуется: ничего. Если на шаге 4 (Доступ) отмечено Нативный доступ к папкам (SAF), приложение использует выбор документов Android; без этого оно использует собственный выбор файла страницы.
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)}`);
}
}Импортируйте резервную копию JSON и обработайте каждый способ, которым pickFile может завершиться ошибкой:
import { pickFile, toast } from '@/lib/appmintNative';
export async function restoreBackup(apply: (data: unknown) => void) {
try {
const f = await pickFile('application/json');
if (!f) return; // cancelled
apply(JSON.parse(await f.text()));
toast(`Restored ${f.name}`);
} catch (e) {
const code = (e as { code?: string }).code;
if (code === 'no-picker') toast('This phone has no file picker app');
else if (e instanceof SyntaxError) toast('That file is not a valid backup');
else toast('Could not open the file');
}
}Выберите текстовый файл и поместите его в редактор рядом со сведениями о файле:
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>
);
}Примечания: В приложении text() читает файл кусками по 4 МБ до конца и декодирует как UTF-8, поэтому большие файлы читаются полностью — держите собственное ограничение размера для того, что может обработать экран. Средство выбора, которое не удалось открыть, отклоняется, поэтому оберните pickFile() тоже в try/catch. В вебе text() — это собственный File.text() браузера. Для бинарных файлов или записи обратно страницы, написанные вручную, используют WebToApkFS.
appmintNative.pip appmintNative#
pip.enter(video?) / pip.exit(video?)
Пример
Держит видео воспроизводимым в небольшом плавающем окне, пока пользователь занят другим.
Возвращает: enter() возвращает Promise<boolean> — true, когда плавающее окно открылось. exit() возвращает Promise<void>. Ни один не отклоняется. Требуется: в приложении Android — опция сборки Картинка в картинке (PiP) на шаге 3 (Интеграция); она включается автоматически, когда ваш код вызывает pip.enter(.
Кнопка «Свернуть» на проигрывателе:
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>
);
}Выходите из режима, когда плеер закрывается (веб):
import { pip } from '@/lib/appmintNative';
declare const video: HTMLVideoElement;
await pip.exit(video);Входите в режим «картинка в картинке» автоматически, когда пользователь уходит из воспроизводимого видео, только там, где это поддерживается:
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]);
}Примечания:
- Android (8+): ВСЁ приложение сворачивается в плавающее окно, а аргумент
videoигнорируется. Возвращаетfalseна более старом Android или в сборке без опции PiP. Пользователь разворачивает окно, чтобы вернуться;exit()там ничего не делает. - Веб: собственный режим «картинка в картинке» элемента
<video>. Видео должно быть загружено (обычно воспроизводиться); вызов безvideoвозвращаетfalse. - Вызывайте по нажатию.
appmintNative.platform appmintNative#
platform(): 'android' | 'ios' | 'web'
Пример
Сообщает, на какой платформе выполняется код: в установленном приложении Android или в веб-браузере.
Возвращает: строку сразу: 'android' внутри приложения, 'web' в браузере. Сначала спрашивает Capacitor (Capacitor.getPlatform()), затем проверяет мост оболочки Android, иначе 'web'. Тип также содержит 'ios', но приложение AppMint его никогда не получает — AppMint собирает только приложения для Android. Требуется: ничего.
Выберите правильную ссылку и скопируйте.
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';Показывайте элементы управления только для приложения.
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>;
}Помечайте события аналитики платформой.
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 }),
});
}Примечания: platform() никогда не отвечает 'web' внутри установленного приложения. Не используйте это, чтобы решать, есть ли функция, — многие функции зависят и от переключателей сборки; на это отвечает can('<capability>').
appmintNative.privacyScreen appmintNative#
privacyScreen — enable() / disable() / isEnabled()
Пример
Блокирует скриншоты, запись экрана и превью в переключателе приложений, пока открыт экран с секретами.
Возвращает: enable() → Promise<boolean> (false, где ничто не может заблокировать); disable() → Promise<void>; isEnabled() → Promise<boolean>. Требуется: ничего. В приложении для Android используется блокировка скриншотов оболочки (скриншоты, запись экрана, трансляция и миниатюра в списке недавних).
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
}, []);
}Сообщите пользователю, что защищено, — только когда это действительно так:
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' : '';
}Включите защиту для одного важного экрана и сообщите пользователю, если применить её не удалось:
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();
}Примечания: В приложении блокировка относится к текущей странице и снимается, когда приложение переходит на другой маршрут, поэтому включите её снова, когда этот экран показан опять. В вебе can('privacyScreen') равно 'none', а enable() возвращает false — не заявляйте там о защите. Ничто не помешает камере, направленной на экран.
appmintNative.pushInbox appmintNative#
import { pushInbox } from '@/lib/appmintNative'
Пример
Возвращает сообщения AppMint Push, полученные этой установкой (сначала новые, до 50), для экрана уведомлений внутри приложения.
Возвращает: PushMessage[] синхронно: { id, title, body, imageUrl, link, receivedAt, read }. Пустой массив в вебе и при выключенном AppMint Push. Нужно: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
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 };
}Покажите счётчик непрочитанного, который следует за новыми поступлениями:
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;
}Отрисуйте список с пустым состоянием для веба и для сборок без AppMint Push и кнопкой «Отметить все как прочитанные»:
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>
);
}Примечания: входящие — это сохранённая истина, читайте их при монтировании; событие appmint:push срабатывает, только пока приложение на экране. Закрытые сообщения не включаются.
appmintNative.readBarcodeFrom appmintNative#
readBarcodeFrom(source: Blob | CanvasImageSource, formats?: BarcodeFormat[]): Promise<{ value, format }[]>
Пример
Читает штрихкоды и QR-коды с картинки, которая уже есть на странице, — файл фото, Blob, <img>, <canvas> или кадр видео. Камера не открывается.
Возвращает: Promise<{ value: string; format: string }[]> — все найденные коды в порядке детектора; пустой массив, если их нет, если изображение не читается или если чтение штрихкодов недоступно. Никогда не отклоняется. Нужно: ничего.
Прочитайте код с фото, которое выбирает пользователь:
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>
);
}Из изображения, которое уже есть на странице:
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);Прочитайте код с кадра canvas, сначала проверив, что поддерживает устройство:
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;
}Примечания:
- Использует
BarcodeDetectorдвижка браузера (есть в WebView приложения для Android; отсутствует в Safari и многих настольных браузерах — тогда возвращает[]). - Обычный
<input type="file" accept="image/*">открывает средство выбора фото Android и не требует разрешения на хранилище. - Сужайте
formats, когда знаете их: так быстрее и меньше ошибок распознавания.barcodeFormats()перечисляет то, что поддерживает устройство.
appmintNative.readOtp appmintNative#
import { readOtp } from '@/lib/appmintNative'
Пример
Ждёт одноразовый код из SMS для этого приложения и возвращает его — панель согласия на SMS от Google в приложении для Android, WebOTP в Chrome в вебе.
Возвращает: Promise<string | null> — код или null, если пользователь отказался, прошло пять минут или signal прервал. Выбрасывает ошибку, когда can('otp') равно 'none'. Требуется: ничего включать не нужно. SMS должно заканчиваться на @<host> #<code> (в приложении для Android хост — 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" />
);
}Обработайте ответ «кода нет» и предложите отправить повторно, когда ожидание закончилось без него:
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>
);
}Перехватывайте выброс исключения, когда сначала нельзя проверить can('otp'):
import { readOtp } from '@/lib/appmintNative';
export async function tryAutoFill(): Promise<string | null> {
try {
return await readOtp({ signal: AbortSignal.timeout(60_000) });
} catch {
return null; // one-time codes unavailable: the user types it
}
}Примечания: это ускоритель — поле всегда должно оставаться доступным для ввода.
appmintNative.recognizeText appmintNative#
import { recognizeText } from '@/lib/appmintNative'
Пример
Читает текст на изображении на устройстве (can('ocr')): распознаватель ML Kit в приложении для Android или TextDetector в Chrome, где он включён.
Возвращает: промис { text, blocks: [{ text, box, lines: [{ text, box }] }] }, рамки в пикселях изображения. Отклоняется с code 'unavailable', где can('ocr') равно 'none', или DOMException при сбое распознавания. Требуется: ничего, разрешение не нужно.
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>
);
}Явно обработайте случаи «нет распознавателя» и сбоя:
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
}
}Читайте нелатинское письмо и перечисляйте каждую строку с её позицией:
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 }));
}Примечания: только латиница. Первый вызов на новом телефоне может занять несколько секунд, пока сервисы Play загружают модель — показывайте прогресс.
appmintNative.remoteConfig appmintNative#
remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()
Пример
Рисует полосу объявлений и экран техобслуживания, которые автор публикует через Remote Update, — для страницы, запущенной в веб-браузере. В приложении Android оболочка делает это сама, поэтому там эти вызовы ничего не делают.
Возвращает: start(manifestUrl?) → Promise<void> (загрузка и отрисовка; никогда не отклоняется); fetch(url) → Promise<RemoteConfig | null> (никогда не отклоняется; null = ничего не применяется); apply(cfg) → функцию отключения; manifestUrl() → string (content из <meta name="appmint-remote-manifest">, '', если его нет); handledHere() → boolean; dismissed(id) → boolean; dismiss(id) → void. Требуется: ничего в приложении — Remote Update там встроен.
Безопасно вызывать при запуске; в приложении возвращает сразу:
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>;
}Или прочитайте значения и нарисуйте собственный интерфейс (только веб):
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)Показывайте текст о техработах сами в вебе, а приложению оставьте его собственный экран:
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;
}Примечания:
RemoteConfig={ version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }.handledHere()равноfalseв приложении для Android: оболочка читает манифест нативно и сама рисует объявление, поэтому вы никогда не увидите два баннера.start()без аргумента используетmanifestUrl(). Сборка AppMint не добавляет этот мета-тег, поэтому без аргумента (или вашего собственного<meta name="appmint-remote-manifest" content="…">) ничего не загружается.fetch()возвращаетnullпри приостановленной или откатённой публикации, манифесте, в спискеplatformsкоторого нетweb, более новом формате манифеста или любой сетевой ошибке.- Режим обслуживания главнее: тогда
apply()закрывает всю страницу текстом об обслуживании. Закрытое объявление остаётся скрытым, пока не опубликован новыйannouncementId.
appmintNative.requestDndAccess appmintNative#
requestDndAccess() — @/lib/appmintNative
Пример
Запрашивает доступ к режиму «Не беспокоить», который Android требует, прежде чем приложение сможет менять громкость звонка или уведомлений при включённом «Не беспокоить».
Возвращает: 'granted', 'opened' (показан экран настроек — проверьте снова, когда пользователь вернётся), 'not_declared' или 'unavailable' в вебе. Требуется: ничего отмечать не нужно — вызов в вашем коде заставляет сборку объявить разрешение.
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';
}Спросите один раз, затем повторите изменение громкости после возвращения пользователя из настроек:
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;
};
}Ветвитесь по каждому возможному ответу:
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>
Пример
Просит Google Play показать собственное окно оценки прямо в приложении (Play In-App Review).
Возвращает: Promise<boolean> — true только когда Play показал панель; false в вебе, для установки не из Google Play, когда Play отказал (квота) или произошёл сбой. Используйте askForReview(), чтобы узнать причину. Требуется: ничего; панель появляется только для приложения, установленного из 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();
}
}Узнайте, почему окно не показалось, не отправляя пользователя никуда:
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);
}Спрашивайте только после момента успеха и не чаще раза в месяц:
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();
}Примечания: Google Play ограничивает частоту показа панели и часто не показывает ничего — никогда не поощряйте отзыв и не делайте кнопку «Оцените нас», которая рассчитывает на открытие панели (для кнопки используйте askForReview({ openStore: true })). Он никогда не уводит из приложения на страницу в Play.
appmintNative.restorePushMessage appmintNative#
import { restorePushMessage } from '@/lib/appmintNative'
Пример
Отменяет dismissPushMessage(id) — сообщение возвращается во входящие AppMint Push на прежнее место.
Возвращает: boolean синхронно — true, если восстановлено; false, если id неизвестен или не был скрыт (в вебе всегда false). Нужно: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
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();
}Предложите «Отменить» сразу после смахивания, в небольшой строке 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>
);
}Проверьте ответ, потому что неизвестный или никогда не закрытый идентификатор даёт 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)`;
}Примечания: сообщения, удалённые через clearPushInbox() или отозванные вами, восстановить нельзя.
appmintNative.safeArea appmintNative#
safeArea(): { top: number; right: number; bottom: number; left: number }
Пример
Возвращает место, занятое строкой состояния, вырезом камеры и панелью навигации, в CSS-пикселях, чтобы закреплённые панели не оказывались под ними.
Возвращает: числа { top, right, bottom, left }, синхронно. Требуется: ничего.
В большинстве случаев достаточно CSS — среда выполнения задаёт --safe-top, --safe-right, --safe-bottom, --safe-left для :root:
export function BottomNav() {
return (
<nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
{/* tabs */}
</nav>
);
}Используйте числа, когда код должен считать, например чтобы разместить плавающую кнопку:
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;
}Чтобы прокручиваемый список не заезжал под панели, задайте отступы по этим числам:
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>;
}Примечания:
- Измеряется по CSS
env(safe-area-inset-*). Значение кэшируется и измеряется заново после изменения размера или поворота окна. - Пока
document.bodyне существует (очень рано при импорте), возвращает одни нули — вызывайте из эффекта. - В браузере на компьютере все четыре обычно
0.
appmintNative.saveFile appmintNative#
saveFile(fileName, data, mime?) from '@/lib/appmintNative'
Пример
Сохраняет текст или Blob как файл, который остаётся у пользователя, с выбранным вами именем. В приложении открывает панель Android «Сохранить как…» с этим именем; в браузере это обычная загрузка.
Возвращает: (синхронно) boolean — true, когда загрузка началась, false, когда нет document или его создание выбросило ошибку. Не говорит, сохранил ли пользователь файл или отменил панель. Требуется: ничего.
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.');
}Blob сохраняет свой тип (третий аргумент игнорируется):
import { saveFile } from '@/lib/appmintNative';
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
saveFile('Backup.json', blob);Сохраните результат запроса и сообщите, если его не удалось начать:
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');
}Примечания: Под капотом это обычный <a download>, который приложение превращает в панель «Сохранить как…» (см. downloads). Всегда передавайте имя с расширением; без него файл сохраняется как download. MIME-тип по умолчанию для строки — text/plain.
appmintNative.saveToGallery appmintNative#
import { saveToGallery } from '@/lib/appmintNative'
Пример
Сохраняет фото или видео в галерею телефона — can('gallery') равно 'native' в приложении для Android и 'web' в браузере (там файл скачивается).
Возвращает: промис URI сохранённого элемента ('' в вебе). Отклоняется с code 'permission-denied', 'download-failed', 'bad-source' или 'failed'. Требуется: ничего; на 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>
);
}Сохраните видеоклип и обработайте коды ошибок, с которыми он может отклониться:
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');
}
}Сохраните изображение из data URL и передайте его в панель «Поделиться», только если оно сохранено:
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' });
}Примечания: source может быть также URL data: или http(s) URL; для URL видео без видео-MIME-типа передайте { kind: 'video' }.
appmintNative.scanBarcode appmintNative#
scanBarcode(video: HTMLVideoElement, opts?: { signal?, formats?, facingMode? }): Promise<{ value, format } | null>
Пример
Открывает камеру в элементе <video>, который ВЫ размещаете в своей вёрстке, и завершается с первым увиденным штрихкодом или QR-кодом.
Возвращает: Promise<{ value: string; format: string } | null> — первый код или null, если отменено (signal прервал), камере отказано в доступе или сканирование недоступно. Никогда не отклоняется и всегда останавливает камеру. Нужно: доступ к камере — в приложении Android включите Камеру на шаге 4 (Доступ) при сборке.
Используйте, только когда видоискатель должен находиться внутри вашего экрана; иначе проще scanCode().
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>
);
}Используйте фронтальную камеру:
import { scanBarcode } from '@/lib/appmintNative';
declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });Закройте сканер после первого срабатывания и предложите ввод кода вручную, когда камера недоступна:
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>
);
}Примечания:
- Вызывает
getUserMedia(пользователь видит запрос разрешения на камеру) и декодирует черезBarcodeDetectorкаждый кадр.can('barcodeScan')равно'none', где чего-то из этого нет. - Сборка сканирует ваш код: вызов
scanBarcode(сам включает Камеру. Если вы отключили её вручную, запрос камеры не удастся, и вы получитеnull. - Запускайте по действию пользователя (кнопка «Сканировать» или открытие экрана сканирования), никогда при запуске приложения. Обрабатывайте
nullкак обычный исход с возможностью ручного ввода.
appmintNative.scanCode appmintNative#
scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>
Пример
Открывает полноэкранный сканер и возвращает первый штрихкод или QR-код — один вызов, рисовать ничего не нужно.
Возвращает: Promise<ScanCodeResult | null> — { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? } или null, когда пользователь закрыл сканер. Всё остальное отклоняется с Error, у которого code равно 'unavailable' (здесь нет сканера — в приложении нет сервисов Play; в вебе нет BarcodeDetector или камеры), 'busy' (сканирование уже открыто), 'camera-denied' (веб: отказано в разрешении на камеру) или 'failed'. Требуется: ничего в приложении (сканеру Google разрешение на камеру не нужно).
По кнопке «Сканировать»:
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>
);
}Используйте разобранные части, которые заполняет приложение (Wi-Fi, ссылки, контакты …), с запасным вариантом ввода вручную в сканере 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);Отложите по кнопке на вашем экране будильника, используя onAlarm, чтобы знать, когда он звонит.
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 отвечает false, когда ничего не звонит, поэтому проверьте, прежде чем обещать пользователю второй звонок.
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.';
}Примечания:
- В приложении это сканер кодов Google (сервисы Play, через
AppMint.scanCodeоболочки): автоматический зум, без запроса камеры.type— это'url','wifi','text','phone','email','sms','geo','contact','calendar','isbn','product'или'driver_license', и заполняется соответствующее поле.manualInputпозволяет пользователю ввести код вручную. - В вебе рисует собственный полноэкранный слой камеры (кнопка закрытия, жест «назад» закрывает его) и декодирует через
BarcodeDetector; там заполняются толькоvalueиformat, а браузер запрашивает разрешение на камеру. nullвсегда означает, что пользователь закрыл сканер; любой другой исход отклоняется сcode, так что покажите сообщение вcatch.- Вызывайте по нажатию, а не при загрузке страницы. Сужайте
formats, когда знаете их.
appmintNative.scheduleAlarm appmintNative#
scheduleAlarm(id, input) — @/lib/appmintNative
Пример
Ставит настоящий будильник из приложения, созданного с помощью ИИ: он звонит на громкости будильника телефона (даже в беззвучном режиме и в режиме «Не беспокоить», если пользователь разрешил будильники), звонит до «Отключить» / «Отложить» или ringSeconds и выводит приложение на экран поверх экрана блокировки.
Возвращает: boolean синхронно — true, если установлен; false в вебе, на iOS или когда alarmSupport() не равно 'ready'. Тот же id заменяет старый будильник. Нужно: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.
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';
}Предлагайте будильник, только когда приложение может его запустить, а иначе объясните, чего не хватает:
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';
}Реагируйте на то, как закончился звонящий будильник, и останавливайте его со своего экрана:
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
};
}Примечания: только в приложении для Android. repeat daily / weekly / weekdays / monthly / yearly сохраняют одно и то же время на часах при переходе на летнее/зимнее время. Отмена через cancelNotification(id) на мосту; результаты приходят через onAlarm.
appmintNative.ScreenOrientation appmintNative#
ScreenOrientation.lock({ orientation }) / ScreenOrientation.unlock()
Пример
Фиксирует экран в портретной или альбомной ориентации для одного экрана в форме плагина ScreenOrientation из Capacitor — тот же код, что и lockOrientation().
Возвращает: Promise<void> в обоих случаях. lock отклоняется, как у Capacitor, когда orientation не является типом блокировки или платформа отказывает в блокировке. Требуется: ничего.
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" />;
}Блокируйте ориентацию только для одного экрана, когда блокировка разрешена, и позвольте пользователю повернуть обратно:
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>;
}Когда нужно знать, сработала ли блокировка, используйте lockOrientation() напрямую (он отвечает false там, где блокировка отклонена, например в настольных браузерах):
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');
}Примечания: orientation — один из типов блокировки Capacitor: 'any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'portrait-secondary', 'landscape-primary', 'landscape-secondary' — и применяется как есть. Настольные браузеры отказывают в блокировке; мобильные браузеры обычно разрешают её только в полноэкранном режиме. lockOrientation(to) возвращает true/false. Дайте пользователю возможность выйти из заблокированного вида.
appmintNative.screenReader appmintNative#
screenReader.isEnabled() / screenReader.speak(text, lang?) / screenReader.onChange(cb)
Пример
Сообщает приложению, включён ли TalkBack, и озвучивает короткие сообщения.
Возвращает: isEnabled() → Promise<boolean> (false в вебе — веб-API не существует). speak() → Promise<void>. onChange(cb) → функцию отписки; cb(enabled) вызывается, когда программа чтения с экрана включается или выключается (только в приложении). Требуется: ничего.
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');
}Объявляйте результат только тем, кто пользуется программой чтения с экрана, а визуальные всплывающие сообщения оставьте для всех остальных:
import { screenReader, toast } from '@/lib/appmintNative';
export async function notifySaved() {
if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
else toast('Note saved');
}Уберите анимацию, пока включён TalkBack, и прекращайте слушать, когда компонент исчезает:
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>;
}Примечания: В приложении speak идёт через плагин ScreenReader; в вебе (или если плагин не сработал) озвучивается синтезом речи. Для большинства объявлений предпочитайте хорошую разметку (области aria-label, aria-live); используйте speak для коротких важных изменений.
appmintNative.searchContacts appmintNative#
searchContacts(query: string, limit = 50): Promise<Contact[]>
Пример
Находит контакты, подходящие под поисковое слово. Только приложение.
Возвращает: Promise<Contact[]> — пусто, когда ничего не подходит, доступ отклонён или Контакты выключены. Требуется: то же, что для listContacts: Контакты на шаге 4 (Доступ) (сборка включает их, когда ваш код это вызывает) и разрешение пользователя при первом использовании.
Поле поиска с небольшой задержкой при вводе.
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);
}Покажите понятное состояние, когда Контакты недоступны и когда ничего не найдено:
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>
);
}Найдите человека по имени, затем заполните для него сообщение:
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
}Примечания: Совпадения ищет поиск по именам в контактах телефона. Каждый контакт — { id?, name?, phones?: string[], emails?: string[], photoUri? }. В вебе can('contactList') равно 'none' — веб-API для чтения адресной книги нет. Контакты — личные данные: укажите их в форме безопасности данных Play и в политике конфиденциальности.
appmintNative.SecureKeyError appmintNative#
class SecureKeyError extends Error { code } — from '@/lib/appmintNative'
Пример
Ошибка, которую бросает secureKeys. Её code — одно короткое слово, по которому можно ветвиться, одинаковое в приложении и в вебе.
Возвращает: Error с name === 'SecureKeyError' и code: 'no-such-key', 'auth-required' (разблокируйте и повторите), 'key-invalidated' (биометрия или блокировка экрана изменились — данные потеряны), 'strongbox-unavailable', 'disabled' (Secure Keys нет в этой сборке / нет Web Crypto), 'bad-alias', 'bad-payload' (невалидный base64 или слишком короткий), 'too-large' (более 4 МБ за один вызов на Android), 'foreign-origin' (запросила страница с другого сайта), 'failed' (всё остальное). Нужно: ничего.
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;
}
}
}Превратите код в короткое сообщение для пользователя в одном месте:
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 + ')';
}
}Считайте подменённые или неверные данные обычным исходом и очистите их, но отсутствующий ключ оставьте как ошибку, которую вы показываете:
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;
}
}Примечания: e.message — это code или "code: detail"; ветвитесь по e.code, а не по тексту сообщения. Собственные слова ошибок платформ превращаются в эти коды, а их исходный текст сохраняется в сообщении. Изменённые или чужие данные дают 'bad-payload' в вебе и 'failed' в приложении, которое не отличает их от других ошибок шифрования.
appmintNative.secureKeys appmintNative#
secureKeys — generate / encrypt / decrypt / remove / has / list / isHardwareBacked
Пример
Ключи шифрования, которые хранит устройство и которые приложение никогда не может экспортировать: Android Keystore в приложении и неизвлекаемый ключ Web Crypto в вебе. Удаление ключа — единственное настоящее «безопасное удаление».
Возвращает: все Promise. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked}; encrypt(alias, text) → строка base64; decrypt(alias, base64) → текст; remove(alias) / has(alias) / isHardwareBacked(alias) → boolean; list() → string[]. generate, encrypt и decrypt бросают SecureKeyError с code. Нужно: Secure Keys (шифрование) на шаге 4 (Доступ) при сборке.
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;
}
}Удалите по-настоящему — после этого никто не сможет прочитать зашифрованные данные:
import { secureKeys } from '@/lib/appmintNative';
export async function wipeDiary() {
await secureKeys.remove('diary');
localStorage.removeItem('diary');
}Выведите список ключей, которые хранит приложение, и покажите, защищён ли каждый аппаратно (веб всегда отвечает 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'}`));
}Примечания: can('secureKeys') — это 'native' в приложении и 'web' в браузере, где isHardwareBacked() всегда false — никогда не говорите там «аппаратный». Ключ с requireAuth: в приложении encrypt/decrypt выбрасывают auth-required, ничего не показывая, после окончания окна разблокировки — вызовите unlock() и повторите; в вебе unlock() показывает сам рантайм. Шифруйте по одной записи (приложение ограничивает вызов 4 МБ). Псевдонимы: буквы, цифры, _ - ., максимум 64.
appmintNative.sendSms appmintNative#
sendSms(phoneNumber: string, message: string): Promise<boolean>
Пример
Отправляет SMS напрямую, без экрана подтверждения. Только приложение Android.
Возвращает: Promise<boolean> — true, если телефон сообщает, что сообщение отправлено, false, если не удалось, если SMS не включены или пользователь отказал в разрешении. Нужно: переключатель SMS (чтение и отправка) на шаге 4 (Доступ) — сборка включает его сама, когда ваш код вызывает это — и разрешение пользователя при первом использовании.
Отправьте оповещение и сообщите пользователю, что произошло.
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';
}
}Сначала запросите подтверждение, потому что сообщение уходит без экрана, где его можно отменить:
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.');
}Отправьте одно и то же уведомление нескольким людям по одному и посчитайте, что было отправлено:
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 };
}Примечания: Обёртка завершается по ПЕРВОМУ ответу, которым является sent (сообщение покинуло телефон), а не delivered. Для отчётов о доставке используйте исходный WebToApk.sendSms с событием appmint:sms. can('smsSend') в вебе равно 'none' (ни один браузер не может отправить SMS). Каждое SMS может стоить пользователю денег. SMS — ограниченное разрешение Google Play: нужна одобренная декларация разрешений в Play Console, поэтому, когда пользователь может сам нажать «отправить», предпочитайте composeSms().
appmintNative.setInAppPresentation appmintNative#
import { setInAppPresentation } from '@/lib/appmintNative'
Пример
Выбирает, кто рисует сообщение в приложении (кампании автора): само приложение ('auto', по умолчанию: баннер, карточка, панель или полноэкранный дизайн автора) или ваш собственный интерфейс ('none', через onInAppMessage).
Возвращает: boolean синхронно: true, если сохранено (остаётся на телефоне). В вебе и без AppMint Push всегда false. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
import { setInAppPresentation } from '@/lib/appmintNative';
// The app draws its own messages from now on.
setInAppPresentation('none');Переключатель в настройках:
import { setInAppPresentation } from '@/lib/appmintNative';
export function onNativePopupsChange(on: boolean) {
if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}Возьмите управление, пока открыт один экран, затем верните его:
import { useEffect } from 'react';
import { setInAppPresentation } from '@/lib/appmintNative';
export function useQuietScreen() {
useEffect(() => {
setInAppPresentation('none');
return () => { setInAppPresentation('auto'); };
}, []);
}Примечания: сообщение, переданное вашему коду, считается показанным. Установка 'none' также закрывает сообщение, которое сейчас на экране.
appmintNative.setMusicVolume appmintNative#
setMusicVolume(percent) — @/lib/appmintNative
Пример
Задаёт громкость зацикленной фоновой музыки приложения от 0 до 100 (значение округляется и ограничивается за вас).
Возвращает: void. Требуется: файл, выбранный в разделе Фоновая музыка на шаге 3 (Интеграция) при сборке. Ничего не делает, когда musicVolume() равно -1.
Приложение запоминает уровень до следующего запуска, поэтому достаточно переключателя отключения звука:
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';
export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40); // remembered by the appПолзунок громкости, который появляется только если приложение содержит музыку:
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); }} />
);
}Приглушите музыку, пока играет видео, и верните потом:
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; }Примечания: 0 делает музыку беззвучной, но она продолжает играть. Уровень сохраняется на телефоне (пока пользователь не очистит данные приложения). Только приложение Android; в вебе ничего не происходит.
appmintNative.setPushPresentation appmintNative#
import { setPushPresentation } from '@/lib/appmintNative'
Пример
Выбирает, кто рисует сообщение AppMint Push, отправленное вами в стиле внутри приложения (баннер, всплывающее окно, нижняя панель или полный экран), пока приложение открыто: оболочка приложения ('auto', по умолчанию) или ваш собственный код ('none').
Возвращает: boolean синхронно — true, если режим сохранён. В вебе всегда false. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке.
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();
}Проверьте ответ, потому что в вебе или при выключенном AppMint Push он равен false:
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;
}Рисуйте свой баннер для каждого сообщения и синхронизируйте входящие:
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); } };
}Примечания: входящие и событие appmint:push работают одинаково в обоих режимах. Установка 'none' также закрывает сообщение, которое оболочка показывает прямо сейчас.
appmintNative.setPushTag appmintNative#
import { setPushTag } from '@/lib/appmintNative'
Пример
Задаёт один тег AppMint Push для этой установки. Центр отправки может отправлять всем, у кого есть тег, а автоматизация по тегу может отправить сообщение через заданное время после установки тега. '' убирает тег.
Возвращает: boolean синхронно — true, если сохранено; false, если ключ не состоит из 1–32 букв, цифр или _, значение длиннее 128 символов или у установки стало бы больше 50 тегов. В вебе всегда false. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке; таргетинг по тегам и автоматизации работают в приложениях, собранных с 30 сентября 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', '');
}Проверьте результат: false означает, что тег отклонён (неверный ключ, значение длиннее 128 символов, больше 50 тегов) или это веб-версия, поэтому ведите собственный учёт состояния.
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;
}Пометьте телефон тегом, когда он достигнет этапа, чтобы центр отправки мог адресовать сообщение всем на этом уровне.
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]);
}Примечания: теги относятся к установке, а не к вошедшему пользователю. Никогда не помещайте в тег личные данные.
appmintNative.setPushTags appmintNative#
import { setPushTags } from '@/lib/appmintNative'
Пример
Задаёт несколько тегов AppMint Push за одно изменение; значение '' убирает этот тег, теги, которые вы не назвали, сохраняются.
Возвращает: boolean синхронно — true, когда сохранено; false, когда какой-либо ключ или значение нарушает правила setPushTag (тогда ничего не сохраняется). В вебе всегда false. Требуется: Android: AppMint Push, включённый на шаге «Интеграция» (шаг 3) при сборке; таргетинг по тегам и автоматизации работают в приложениях, собранных с 30 сентября 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('');
}Пометьте телефон выбранным языком и темами, когда пользователь сохраняет настройки, и проверьте, что каждый тег принят.
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;
}Синхронизируйте теги с вошедшим аккаунтом в эффекте React, удаляя те, что больше не нужны.
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]);
}Примечания: всё из одного вызова сохраняется целиком или не сохраняется вовсе.
appmintNative.setPushUser appmintNative#
import { setPushUser } from '@/lib/appmintNative'
Пример
Сообщает AppMint Push, какой из ваших пользователей вошёл на этой установке, чтобы центр отправки мог отправить сообщение именно ему. После входа передайте свой постоянный id аккаунта, а при выходе — ''.
Возвращает: boolean синхронно — true, если id сохранён; false для id с чем-либо, кроме букв, цифр и _ @ . : + - (до 128). В вебе всегда false. Требуется: Android: включённый AppMint Push на шаге «Интеграция» (шаг 3) при сборке; отправка конкретному пользователю работает в приложениях, собранных с 30 сентября 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();
}, []);
}Обработайте отклонённый идентификатор: setPushUser отвечает false для адреса электронной почты или любого идентификатора с символами вне допустимого набора, поэтому отправляйте стабильный идентификатор.
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;
}Сбрасывайте идентификатор, когда пользователь выходит из аккаунта, в обычном обработчике клика.
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>
);
}Совместите это с тегом, чтобы один шаг входа определял пользователя и его тариф вместе.
import { setPushUser, setPushTag } from '@/lib/appmintNative';
export function onSignedIn(userId: string, plan: 'free' | 'pro') {
setPushUser(userId);
setPushTag('plan', plan);
}Примечания: идентификатор относится к установке, поэтому при выходе из аккаунта его нужно сбросить. Теги он не сбрасывает — вызовите setPushTags со значениями '' для тегов конкретного пользователя.
appmintNative.setStatusBar appmintNative#
setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void
Пример
Окрашивает системную строку состояния под экран и выбирает для неё светлые или тёмные значки.
Возвращает: ничего. Требуется: ничего.
style — это цвет ЗНАЧКОВ: 'light' = светлые значки для тёмной панели (по умолчанию), 'dark' = тёмные значки для светлой панели. Вызовите один раз при запуске с --bg приложения и снова на любом экране с другим фоном:
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;
}Светлый экран:
import { setStatusBar } from '@/lib/appmintNative';
setStatusBar('#FFFFFF', 'dark');Форма в стиле Capacitor (стиль назван по ФОНУ: 'DARK' = светлые значки):
import { StatusBar } from '@/lib/appmintNative';
await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });Примечания:
- В приложении для Android оболочка сама красит панель, что работает и на Android 15 edge-to-edge; не разбираемый цвет игнорируется — передавайте
#RRGGBB. Вызов только со стилем (backgroundColor = null) сохраняет цвет, который страница задала последним, и переключает значки; пока страница не задала цвет, он переключает значки через плагин StatusBar, а панель сохраняет цвет, выбранный при сборке. - В вебе устанавливает
<meta name="theme-color">; цвет значков браузер выбирает сам.
appmintNative.setVolume appmintNative#
setVolume(stream, percent, showUi?) — @/lib/appmintNative
Пример
Задаёт собственный уровень громкости телефона для одного потока, 0–100. showUi показывает ползунок громкости телефона при его движении.
Возвращает: { ok, stream?, current?, max?, percent?, error? } — error равен unknown_stream, bad_percent, fixed_volume, dnd_access_required («Не беспокоить» блокирует звонок / уведомление — вызовите requestDndAccess()), muted («Не беспокоить» в режиме «Полная тишина» заглушает поток, поэтому уровень не изменился), not_applied, foreign_origin, failed или unavailable в вебе. Нужно: для медиа, будильника и звонка ничего.
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();
}}
/>
);
}Покажите громкость медиа в виде ползунка, который следует за аппаратными клавишами, с помощью 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))} />;
}Повысьте громкость будильника перед тем, как зазвонит экран будильника, и сообщите, почему она не изменилась.
import { setVolume } from '@/lib/appmintNative';
export function louderAlarm() {
const r = setVolume('alarm', 90, true); // true shows the phone's volume slider
if (!r.ok) {
switch (r.error) {
case 'fixed_volume': return 'This phone has a fixed volume.';
case 'unavailable': return 'Volume control needs the Android app.';
default: return `Could not change the volume (${r.error}).`;
}
}
return `Alarm volume is now ${r.percent}%`;
}appmintNative.sheet appmintNative#
sheet(opts: { title?, content, dismissible? }): { close(), element, closed }
Пример
Открывает модальную нижнюю панель с настоящим поведением панели — для панели фильтров, редактора или предпросмотра деталей.
Возвращает: дескриптор: close() закрывает окно, element — элемент тела (заполняйте или обновляйте его), closed — Promise<void>, который выполняется, когда окно закрылось, как бы оно ни закрылось. Нужно: ничего.
Параметры: title (необязательный заголовок), content — элемент, простой текст (не HTML) или функция (host) => { … }, которая заполняет тело; dismissible (по умолчанию true: нажатие на фон, свайп вниз, Escape и жест «назад» закрывают панель).
Обычное содержимое DOM:
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;Содержимое React, отрисованное в панели через портал:
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,
)}
</>
);
}Панель, которая остаётся открытой, пока пользователь не выберет, с использованием dismissible: false и promise closed.
import { sheet } from '@/lib/appmintNative';
export async function confirmDelete(name: string): Promise<boolean> {
let answer = false;
const h = sheet({
title: 'Delete this item?',
dismissible: false, // only the buttons below close it
content: (host) => {
const p = document.createElement('p');
p.textContent = `"${name}" will be removed.`;
const yes = document.createElement('button');
yes.textContent = 'Delete';
yes.onclick = () => { answer = true; h.close(); };
const no = document.createElement('button');
no.textContent = 'Keep it';
no.onclick = () => h.close();
host.append(p, yes, no);
},
});
await h.closed; // resolves however the sheet closed
return answer;
}Примечания:
- Рисуется в токенах приложения (
--surface,--ink,--muted,--border), выше безопасной зоны, с лёгким тактильным откликом при открытии. - Свайп вниз закрывает панель, если потянуть дальше 120 px или резко смахнуть; тело сначала прокручивается, если оно не в самом верху.
- При
dismissible: falseзакрыть его может только ваш собственныйclose()— дайте пользователю кнопку. - Для простого списка действий используйте
actionSheet().
appmintNative.shortcuts appmintNative#
shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()
Пример
Добавляет ярлыки в меню по долгому нажатию на значок приложения (динамические ярлыки Android). Нажатие открывает приложение на этом маршруте.
Возвращает: set() возвращает Promise<boolean> — true, когда ярлыки переданы системе, false там, где их нет (веб). clear() возвращает Promise<void>. Требуется: ничего.
Задайте их один раз при запуске и направляйте по ссылке запуска:
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]);
}Удалите их (например, после выхода из аккаунта):
import { shortcuts } from '@/lib/appmintNative';
await shortcuts.clear();Откройте нужный экран, когда нажат ярлык при уже работающем приложении, через 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]);
}Показывайте ярлыки только вошедшему пользователю и обрабатывайте веб, где set() отвечает 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]);
}Примечания:
- Не больше 4 пунктов; лишние отбрасываются. К
routeбез начального/он добавляется.idнеобязателен. - Android 7.1+; на более старых версиях Android вызов ничего не делает. Заголовки обрезаются до 25 символов (короткая метка) и 50 (длинная метка).
- Нажатие открывает приложение со ссылкой запуска
appmint-shortcut://<package><route>, поэтомуnew URL(u).pathname— ваш маршрут. Читайте её черезlaunchUrl()при монтировании и черезonDeepLink(), пока приложение работает. can('shortcuts')в вебе —'none': у браузера нет значка приложения.
appmintNative.snoozeAlarm appmintNative#
snoozeAlarm(id?, minutes?) — @/lib/appmintNative
Пример
Останавливает звонящий будильник и звонит снова через minutes (0 или не указано = собственное snoozeMinutes будильника, по умолчанию 10). Завтрашний звонок повторяющегося будильника не сдвигается.
Возвращает: boolean — true, если будильник звонил и отложен. Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.
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);
});Отложите по кнопке на вашем экране будильника, используя onAlarm, чтобы знать, когда он звонит.
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 отвечает false, когда ничего не звонит, поэтому проверьте, прежде чем обещать пользователю второй звонок.
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
Пример
Читает текст вслух движком синтеза речи телефона.
Возвращает: void — начинает говорить (или встаёт в очередь после текущей речи) и не подаёт сигнала «закончено». opts: { lang?: string; rate?: number; pitch?: number } (по умолчанию: язык движка, 1, 1). Требуется: ничего.
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>
);
}Разные языки и скорости:
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Произнесите короткое подтверждение после действия и сообщите пользователю ещё и текстом.
import { speak, toast } from '@/lib/appmintNative';
export function announceSaved(title: string) {
toast('Saved');
speak(`${title} saved`, { rate: 1.1 });
}Читайте список по одному пункту; speak() ставит в очередь, поэтому вызывайте его в цикле и останавливайте всё через stopSpeaking(), когда экран закрывается.
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>;
}Примечания: В приложении Android: собственный движок приложения. В вебе: speechSynthesis. Несколько вызовов speak() воспроизводятся один за другим. Если нужно знать, когда речь закончилась (чтобы подсветить слова или перейти дальше), используйте window.speechSynthesis с onend фразы напрямую; это работает и в приложении Android.
appmintNative.speech appmintNative#
speech — @/lib/appmintNative
Пример
Диктовка: превращает речь пользователя в текст. Распознаватель речи Android в приложении для Android, Web Speech API в Chrome.
Возвращает: speech.isAvailable() → Promise<boolean>. speech.start(onResult, opts?) → Promise<boolean>: true, если прослушивание началось, false, если в разрешении на микрофон отказано или диктовка недоступна. onResult(text, isFinal) вызывается с текстом. speech.stop() → Promise<void>. opts: { lang?: string; partial?: boolean; prompt?: string }. Нужно: Микрофон на шаге 4 (Доступ); сборки на основе ИИ включают его сами, если код использует распознавание речи.
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>
);
}Диктуйте в текстовое поле и сообщайте, если в доступе к микрофону отказано, поскольку start() отвечает 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>
);
}Прекратите слушать, когда экран закрывается, и прочитайте результат через 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>
);
}Примечания:
- При включённом
partial(по умолчанию)onResult(text, false)приходит по мере поступления слов, затем один разonResult(text, true)с итоговым текстом. Сpartial: falseвы получаете только итоговый — когда пользователь замолчал. - Запрос разрешения показывается при первом
start(). - Если пользователь ничего не говорит,
onResultне вызывается; прослушивание просто заканчивается. Оставьте в интерфейсе кнопку «Стоп» или тайм-аут. - Прослушивание само прекращается после паузы.
langпо умолчанию — язык телефона.
appmintNative.splash appmintNative#
splash.hold() / splash.ready()
Пример
Удерживает заставку запуска на экране, пока первый настоящий экран не готов, вместо «заставка, затем индикатор загрузки».
Возвращает: ничего (оба синхронные). Требуется: ничего.
По умолчанию заставка скрывается сама при первой отрисовке приложения — вам ничего не нужно. Когда первому экрану нужны данные, удержите заставку в начале модуля и снимите её, когда этот экран показан:
// 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>;
}Удерживайте заставку, пока восстанавливается сессия вошедшего пользователя, и снимайте её в finally, чтобы сбой не оставил её на экране.
// main.tsx
import { splash } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';
splash.hold();
supabase.auth.getSession().finally(() => splash.ready());Снимите один раз, из хука, после того как данные первого экрана появились на экране.
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]);
}Примечания:
- В приложении для Android
hold()велит оболочке удерживать заставку;ready()скрывает её (минимальное время заставки, заданное создателем, всё равно действует). - В вебе оба ничего не делают — заставки там нет.
- Оболочка ограничивает удержание примерно 6 секундами, поэтому страница, которая никогда не вызывает
ready(), всё равно откроется.
appmintNative.StatusBar appmintNative#
StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })
Пример
Строка состояния в форме плагина StatusBar из Capacitor, построенная на setStatusBar().
Возвращает: Promise<void> в обоих случаях. Нужно: ничего.
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' });Цвет и значки вместе — лучше используйте setStatusBar(), который задаёт оба одним вызовом:
import { setStatusBar } from '@/lib/appmintNative';
setStatusBar('#0B0F1A', 'light'); // dark bar, light icons
setStatusBar('#FFFFFF', 'dark'); // light bar, dark iconsПереключайте панель вместе со светлой и тёмной темой приложения, используя вызовы в стиле 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]);
}Примечания: style следует Capacitor: 'DARK' означает тёмный фон (светлые значки), всё остальное означает тёмные значки. В приложении для Android панель красит мост оболочки (вызов цвета в плагине на Android 15+ ничего не делает), а setStyle() переключает значки по текущему цвету панели. В вебе цвет становится <meta name="theme-color">.
appmintNative.stopAlarm appmintNative#
stopAlarm(id?) — @/lib/appmintNative
Пример
Останавливает звонящий будильник, как его кнопка «Отключить». Повторяющийся будильник остаётся установленным на следующий раз.
Возвращает: boolean — true, когда будильник звонил и теперь остановлен. id опущен = тот, что звонит. Требуется: 🔔 Будильник на шаге «Доступ» (шаг 4) при сборке.
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>
);
}Остановите будильник с экрана, который приложение открывает при звонке, и реагируйте на каждый способ его завершения:
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 ?? ''); },
};
}Проверьте ответ: false означает, что ничего не звонило (или это веб), поэтому не сообщайте, что будильник остановлен:
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
Пример
Немедленно останавливает синтез речи и сбрасывает всё, что ещё ждёт озвучивания.
Возвращает: void. Требуется: ничего.
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>
);
}Прочитайте длинный текст вслух на языке и с той скоростью, что выбрал пользователь, и остановите чтение одной кнопкой:
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>
);
}Прервите голос, когда пользователь начинает печатать ответ:
import { stopSpeaking } from '@/lib/appmintNative';
export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}Примечания: Безопасно вызывать, когда ничего не озвучивается. В приложении для Android: остановка нативного движка. В вебе: speechSynthesis.cancel().
appmintNative.swipeable appmintNative#
swipeable(el: HTMLElement, opts: SwipeOptions): () => void
Пример
Добавляет жесты смахивания к элементу — строки с удалением смахиванием, листание, закрытие карточки.
Возвращает: функцию отписки, которая также восстанавливает исходный touch-action элемента. Требуется: ничего.
Параметры: onLeft, onRight, onUp, onDown (хотя бы один), threshold (px пути, по умолчанию 56), edgeGuard (px от левого/правого края экрана, где горизонтальные свайпы игнорируются, по умолчанию 24), haptics (тактильный отклик выбора при свайпе, по умолчанию true).
Смахните влево для удаления, с видимой кнопкой для того же действия:
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>
);
}Переключайтесь между вкладками влево / вправо:
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();Закройте карточку смахиванием вверх, удалив обработчик при размонтировании:
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>;
}Примечания:
- Построено на Pointer Events, поэтому один и тот же код работает с касанием, пером и перетаскиванием мышью.
- Он задаёт
touch-actionза вас:pan-yдля горизонтальных обработчиков (вертикальная прокрутка продолжает работать),pan-xдля вертикальных,noneдля обоих. - Свайп должен явно идти в одном направлении (в 1,5 раза больше по одной оси); диагональные прокрутки не срабатывают. Второй палец отменяет жест.
- Защита краёв нужна, потому что системный жест «назад» Android занимает края экрана.
appmintNative.systemPalette appmintNative#
systemPalette(): Promise<SystemPalette>
Пример
Читает цвета Material You телефона (строятся по обоям на Android 12+), чтобы акцент приложения мог следовать обоям пользователя, как в системных приложениях.
Возвращает: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> — каждый образец сопоставляет тон ('0', '10', '50', '100' … '900', '1000') с hex-цветом. { available: false } на Android ниже 12 и в вебе. Требуется: ничего.
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']);
}Используйте цвета обоев только когда телефон их предлагает, и читайте их заново, когда приложение возвращается на экран:
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)
}Показывайте переключатель «Подобрать под мои обои», только когда палитра существует, считая { available: false } ответом «здесь нет»:
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>;
}Примечания: Проверьте контраст, прежде чем использовать тон обоев для текста или кнопок — --accent всё равно должен давать 3:1 на вашем фоне. Предлагайте это как опцию («Подстроить под мои обои»), а не как принудительную тему. Вызывайте снова при возобновлении работы приложения: пользователь мог сменить обои.
appmintNative.TextToSpeech appmintNative#
TextToSpeech — @/lib/appmintNative
Пример
Объект TextToSpeech в стиле Capacitor (speak / stop), работающий на том же коде, что и speak() и stopSpeaking().
Возвращает: speak({ text, lang?, rate?, pitch? }) → Promise<void>, завершается, когда текст произнесён (или stop() его прервал), и отклоняется, когда произнести не удалось. stop() → Promise<void>. Требуется: ничего.
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();
}Читайте вслух список строк одну за другой; каждый speak завершается, когда строка поставлена в очередь, поэтому движок воспроизводит их по порядку:
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' });
}
}Кнопка «Прочитать вслух», которая прекращает озвучивание при уходе с экрана:
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>;
}Примечания: await TextToSpeech.speak(…) — сигнал завершения: следующий шаг после озвучивания может идти сразу за ним. В приложении: нативный движок синтеза речи Android в оболочке; в вебе: speechSynthesis.
appmintNative.textZoom appmintNative#
textZoom.get() / textZoom.getPreferred() / textZoom.set(value)
Пример
Элемент управления размером текста в приложении. В приложении он меняет масштаб текста страницы (1 = 100%); в вебе он меняет размер корневого шрифта, который масштабирует любую вёрстку на основе rem.
Возвращает: get() → Promise<number>. getPreferred() → Promise<number> — размер, выбранный пользователем в настройках телефона (1 в вебе). set(value) → Promise<void>; значение ограничивается диапазоном 0,5–3, и вызов отклоняется, если значение не число. Нужно: ничего.
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()); }Запомните выбор между запусками:
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));
}Кнопки изменения размера текста, которые читают текущее значение, держат его в допустимом диапазоне и показывают в процентах:
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>
);
}Примечания: Приложение уже учитывает настройку размера шрифта телефона; это дополнительный элемент управления внутри приложения. Проверьте вёрстку при больших размерах — фиксированная высота у текстовых элементов обрезает текст.
appmintNative.toast appmintNative#
toast(message: string, opts?: { duration?: 'short' | 'long' }): void
Пример
Показывает краткое сообщение, не требующее ответа, — «Скопировано», «Сохранено», «Добавлено в список».
Возвращает: ничего (запустил и забыл). Требуется: ничего.
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>
);
}Более длинное сообщение остаётся около 3,5 секунды:
import { toast } from '@/lib/appmintNative';
toast('Your export will be ready in a minute', { duration: 'long' });Форма в стиле Capacitor Toast.show({ text, duration? }) делает то же самое и возвращает Promise<void>:
import { Toast } from '@/lib/appmintNative';
await Toast.show({ text: 'Added to list' });Для сообщения, которое пользователь ОБЯЗАН увидеть, используйте вместо этого диалог:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });Примечания:
- В приложении для Android плагин Capacitor Toast показывает его внизу экрана.
- Во всех остальных случаях это небольшая плашка (snackbar), нарисованная поверх страницы в цветах приложения (
--inkна--bg), выше отступа безопасной зоны, сrole="status", чтобы программы чтения с экрана её озвучивали. - Одновременно показывается только одно; новое всплывающее сообщение заменяет старое.
- Всплывающее сообщение легко пропустить — так задумано. Никогда не используйте его для ошибки, на которую пользователь должен отреагировать.
appmintNative.Toast appmintNative#
import { Toast } from '@/lib/appmintNative'
Пример
Описано вместе с appmintNative.toast — пример там показывает и этот.
Показывает краткое сообщение, не требующее ответа, — «Скопировано», «Сохранено», «Добавлено в список».
Возвращает: ничего (запустил и забыл). Требуется: ничего.
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>
);
}Более длинное сообщение остаётся около 3,5 секунды:
import { toast } from '@/lib/appmintNative';
toast('Your export will be ready in a minute', { duration: 'long' });Форма в стиле Capacitor Toast.show({ text, duration? }) делает то же самое и возвращает Promise<void>:
import { Toast } from '@/lib/appmintNative';
await Toast.show({ text: 'Added to list' });Для сообщения, которое пользователь ОБЯЗАН увидеть, используйте вместо этого диалог:
import { Dialog } from '@/lib/appmintNative';
await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });Примечания:
- В приложении для Android плагин Capacitor Toast показывает его внизу экрана.
- Во всех остальных случаях это небольшая плашка (snackbar), нарисованная поверх страницы в цветах приложения (
--inkна--bg), выше отступа безопасной зоны, сrole="status", чтобы программы чтения с экрана её озвучивали. - Одновременно показывается только одно; новое всплывающее сообщение заменяет старое.
- Всплывающее сообщение легко пропустить — так задумано. Никогда не используйте его для ошибки, на которую пользователь должен отреагировать.
appmintNative.torch appmintNative#
import { torch } from '@/lib/appmintNative'
Пример
Фонарик телефона: torch.on(), off(), toggle(), isOn(), isAvailable(). can('torch') в вебе равно 'none'.
Возвращает: on()/off() → Promise (отклоняется с code 'unavailable' или 'CAMERA_IN_USE'); toggle() → новое состояние; isOn() / isAvailable() → boolean. Нужно: ничего, разрешение не требуется.
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>
);
}Выключайте свет, когда пользователь уходит с экрана, и показывайте ошибку, которую сообщает телефон, если включить или выключить его не удалось:
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>;
}Мигните светом как сигналом, затем оставьте его выключенным:
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));
}
}Примечания: выключается при закрытии приложения. Пока открыт собственный поток камеры приложения, включайте свет через ограничение torch дорожки.
appmintNative.trackEvent appmintNative#
import { trackEvent } from '@/lib/appmintNative'
Пример
Сообщает о моменте в вашем приложении, чтобы показывались сообщения автора внутри приложения с триггером «Когда происходит событие» (с тем же именем). Приложение скачивает кампании автора из центра отправки AppMint Push и показывает их само.
Возвращает: boolean синхронно: false для недопустимого имени (буквы, цифры и _ . : -, до 64), в вебе и в приложениях, собранных без AppMint Push. Требуется: Android: AppMint Push, включённый на шаге «Интеграция» (шаг 3) при сборке.
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
}Сообщите о смене экрана, которую URL не показывает, чтобы её видели сообщения «На странице»:
import { trackEvent } from '@/lib/appmintNative';
export function selectTab(tab: 'home' | 'cart' | 'profile') {
setActiveTab(tab);
trackEvent('appmint:route', { path: '/' + tab });
}Хук, отслеживающий первое открытие экрана:
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]);
}Примечания: изменения URL (загрузки страниц, pushState, маршруты #hash) уже видны; сообщайте только то, что адрес не меняет.
appmintNative.transition appmintNative#
transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>
Пример
Выполняет смену экрана с нативной анимацией: push сдвигает новый экран справа, pop сдвигает назад, fade плавно сменяет.
Возвращает: Promise<void>, который выполняется после изменения и его анимации. update — ваше изменение состояния (navigate роутера, setState), оно применяется всегда. Нужно: ничего.
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>;
}Плавный переход между двумя вкладками:
import { transition } from '@/lib/appmintNative';
declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });Используйте плавное появление при смене вкладки, по одному обработчику на вкладку:
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>
);
}Примечания:
- Использует View Transitions API, если он есть в WebView. Иначе применяет обновление и проигрывает короткую анимацию появления на
#root(или на первом дочернем элементе<body>). - Когда пользователь попросил систему об уменьшении движения, обновление применяется без анимации.
- Анимации длятся около 240 мс.
appmintNative.unlock appmintNative#
unlock(title?, subtitle?) — from '@/lib/appmintNative'
Пример
Просит владельца телефона подтвердить своё присутствие — отпечатком пальца, лицом или PIN-кодом/паролем устройства — и сообщает вам, подтвердил ли он.
Возвращает: Promise<boolean> — true, если разблокировано; false, если отменено, не удалось, превышено число попыток или на этом устройстве нечем проверять. Никогда не бросает исключение. Нужно: в приложении Отпечаток пальца на шаге 4 (Доступ) при сборке. В вебе: платформенный аутентификатор браузера (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();
}Скройте кнопку там, где нечем разблокировать:
import { can } from '@/lib/appmintNative';
const showLockButton = can('unlock') !== 'none'; // 'native' | 'web' | 'none'Защитите удаление свежей разблокировкой, затем подтвердите тактильным откликом и всплывающим сообщением:
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');
}Примечания: в приложении PIN-код, графический ключ или пароль устройства разрешены как второй способ входа. В вебе первый вызов регистрирует платформенный аутентификатор браузера и считается разблокированным. Это доказывает присутствие владельца устройства; это НЕ вход в аккаунт — никогда не используйте это как единственную защиту данных, хранящихся на сервере. На Android 11 и новее окно открывается на любом телефоне с блокировкой экрана — телефон только с PIN-кодом тоже его получает и разблокируется PIN-кодом; на Android 10 и старше сначала нужно сохранить отпечаток, иначе он отвечает false без запроса.
appmintNative.updates appmintNative#
updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()
Пример
Обновитель на стороне страницы для обновлений веб-пакета. Коду страницы он не нужен: в приложении AppMint Remote Update выполняет нативно приложение для Android, а updates.supported() везде возвращает false.
Возвращает: supported() → boolean (сейчас всегда false); download(url, sha256, version, binaryVersion) → Promise<boolean> (false, пока не поддерживается); stage(dir, version, binaryVersion) → Promise<boolean>; confirm() → void; revertIfUnconfirmed(binaryVersion) → Promise<boolean>; current() → { path, version, binaryVersion } | null. Нужно: для вашего приложения ничего — обновления публикуются из Remote Update в AppMint.
В коде приложения безопасно только проверять и ничего не делать, когда это выключено:
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);
}Покажите, какой пакет запущен (строка «О приложении» в настройках):
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';Покажите версию запущенного контента в строке настроек со встроенной подписью, когда у страницы нет подготовленного пакета:
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>;
}Примечания:
- В приложении для Android менеджер Remote Update оболочки скачивает и проверяет обновления, отдаёт их и откатывает пакет, который не загрузился. Страницу отдаёт сама оболочка, поэтому пакет, подготовленный из кода страницы, никогда бы не показался — поэтому
supported():false. - В вебе обновлять нечего; страница — это то, что отправляет сервер.
- Обновления могут менять только HTML, CSS и JavaScript; для новой нативной функции по-прежнему нужна новая сборка приложения.
appmintNative.watchIdle appmintNative#
import { watchIdle } from '@/lib/appmintNative'
Пример
Сообщает приложению, когда пользователь бездействует или блокирует экран, и когда он вернулся (can('idle')). В первый раз запускайте по нажатию — для запроса разрешения оно нужно.
Возвращает: Promise функции остановки. Обратный вызов получает { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } при старте и при каждом изменении. Отклоняется с code 'permission-denied' или 'unavailable'. Нужно: ничего, разрешение 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>
);
}Покажите состояние «отошёл» и прекратите наблюдение при размонтировании; отказ в разрешении сообщается с его 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 };
}Приостановите таймер опроса, пока пользователь отсутствует, и обновите данные, когда он вернётся:
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); };
}Примечания: порог не меньше одной минуты (нижняя граница по спецификации).
appmintNative.widget appmintNative#
widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()
Пример
Заполняет виджет главного экрана приложения — одну карточку с заголовком, крупным значением, подписью, до двенадцати строк (у каждой необязательные цвет, маркер и маршрут) и нажатием, которое открывает приложение по маршруту.
Возвращает: set(card) → Promise<boolean> (true, если сохранено); clear() → Promise<void>; placedCount() → Promise<number> (сколько копий разместил пользователь); requestPin() → Promise<boolean> (true, если открылся процесс «добавить виджет» лаунчера). Ни один из них не отклоняется. Нужно: в приложении Android ничего (виджет всегда входит в сборку).
Обновляйте карточку после каждого изменения, которое пользователь захочет увидеть на главном экране:
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
});
}Повестка, которая заполняет виджет с изменённым размером, с прозрачным фоном и просроченной строкой красным цветом:
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',
});
}Кнопка «Добавить на главный экран»:
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>
);
}Очистите карточку, когда пользователь выходит из аккаунта, и покажите, сколько копий на главном экране:
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>;
}Примечания:
- Карточка сохраняется на устройстве, переживает перезагрузку, и каждая размещённая копия обновляется.
placedCount()равное0означает, что на главном экране ещё нет ни одной копии. - Поля карточки:
title(пусто = название приложения),value(крупно вaccent, скрыто, если пусто),caption(мелкий текст под значением),lines(максимум 12; шесть показываются всегда, больше — когда виджет достаточно высокий; строка — это string или{ text, color?, marker?, bold?, wrap?, route? }),route(по умолчанию/),textSize('auto'увеличивает текст вместе с виджетом, пока все строки помещаются, перенося длинные строки на две;'small' | 'normal' | 'large' | 'xl'или число 0,6–3),align('start' | 'center' | 'end'),wrap(длинные строки занимают две строки), а такжеbg/ink/accent. Цвета по умолчанию берутся из токенов CSS--bg,--ink,--accent; они должны быть#RRGGBB,#AARRGGBB(альфа ПЕРВОЙ:#9914264A— 60 % непрозрачности),#RGB,rgb()/rgba()или названием цвета — значениеhsl()/oklch()откатывается к цветам по умолчанию.set()возвращаетfalseдля неизвестногоtextSize/alignили неверного цвета строки. - Нажатие открывает приложение с
appmint-shortcut://<package><route>; прочитайте это черезlaunchUrl()/onDeepLink(). requestPin()требуется Android 8+ и лаунчер, который это поддерживает.clear()показывает пустую карточку «Пока нечего показать».- Только в приложении для Android:
can('widget')равно'none'в вебе, гдеset()иrequestPin()возвращаютfalse.