ساخت APK رایگان

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: پل پوسته آن را مستقیم ذخیره می‌کند (صفحهٔ Contacts باز نمی‌شود).

مقدار بازگشتی: 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>;
}

نکته‌ها: فوراً ذخیره می‌کند - ویرایشگر مخاطب باز نمی‌شود - پس ابتدا در صفحه خودتان با کاربر تأیید کنید. شماره‌ها را به‌عنوان شماره موبایل و مخاطب را روی دستگاه ذخیره می‌کند (بدون حساب). نوشتن مخاطبین حساس است: آن را در فرم ایمنی داده‌های Play و سیاست حفظ حریم خصوصی خود اعلام کنید.

appmintNative.alarmSupport appmintNative#

alarmSupport() — @/lib/appmintNative

نمونه

اینکه آیا scheduleAlarm همین حالا می‌تواند آلارم تنظیم کند، و اگر نه چه چیزی کم است.

مقدار بازگشتی: 'ready'، 'not_enabled' (بدون ساعت زنگ‌دار ساخته شده)، 'needs_exact_alarm' (اندروید 12 به بالا: آلارم‌های دقیق خاموش‌اند)، 'needs_notification_permission' (اندروید 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)

نمونه

نام اپلیکیشن، شناسه بسته و نسخه، راهی برای بستن اپلیکیشن، و رویدادهای اپلیکیشن - به شکل افزونه App در Capacitor.

مقدار برگشتی: getInfo() ← Promise<{ name: string; id: string; build: string; version: string }>. exitApp() ← void. addListener(event, fn) ← شکل Capacitor (یک Promise از handle که remove() هم دارد). رویدادها: appStateChange ({ isActive })، pause، resume (رفتن صفحه به پس‌زمینه / برگشت از آن)، appUrlOpen ({ url }، یک پیوند عمیق - همان 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 پوسته استفاده می‌کند و در وب کاری نمی‌کند، پس هرگز خروج را تنها راه ادامه نکنید. برای رویدادهای pause/resume و پیوندهای عمیق 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.');
}

نکته‌ها: در اپلیکیشن از افزونه AppLauncher در Capacitor استفاده می‌شود. در اندروید 11 به بالا canOpen فقط طرح‌هایی را می‌بیند که اپلیکیشن اعلام کرده است (https، geo، google.navigation، tel، mailto، sms، whatsapp، intent، upi)؛ برای بقیه حتی وقتی اپلیکیشن نصب است false پاسخ می‌دهد - به‌جای آن open() را فراخوانی کنید و نتیجه‌اش را بررسی کنید. در وب یک طرح سفارشی قابل بررسی نیست، پس canOpen فقط برای http(s) برابر true است و open به پیوند می‌رود.

appmintNative.appLock appmintNative#

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

نمونه

وقتی کاربر بعد از مدتی در پس‌زمینه بودن برمی‌گردد اپلیکیشن را دوباره قفل می‌کند: اثر انگشت / چهره / PIN می‌خواهد (unlock())، و صفحه حریم خصوصی را روشن می‌کند تا تغییردهنده اپلیکیشن محتوای شما را نشان ندهد.

مقدار برگشتی: تابعی که قفل را متوقف می‌کند (هنگام خروج از حساب یا unmount فراخوانی کنید). onLocked(true) وقتی اپلیکیشن قفل می‌شود، onLocked(false) بعد از باز کردن موفق قفل اجرا می‌شود. نیازمندی: همان unlock() - اندروید: Fingerprint هنگام ساخت تیک خورده باشد.

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 Lock در ویزارد ساخت است که اپلیکیشن هنگام اجرا به‌صورت بومی نشان می‌دهد.

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

نکته‌ها: بعد از یک لحظهٔ موفقیت و به‌ندرت بپرسید. نصب آزمایشی sideload همیشه پاسخ می‌دهد 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 (درخواست تکرارشونده، دست‌کم هر ۱۵ دقیقه)، 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');

رویداد خام (چیزی که پوشش در اندروید منتظرش است):

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 شروع نمی‌شود، شناسه خالی، یا سیستم رد کرد) یا در 10 ثانیه پاسخ نداد. جاوااسکریپت صفحه هرگز در پس‌زمینه اجرا نمی‌شود؛ فقط همین درخواست اجرا می‌شود. پاسخ 4xx دوباره امتحان نمی‌شود؛ خطاهای شبکه و 5xx تا 8 بار تکرار می‌شوند.

appmintNative.backgroundTasks appmintNative#

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

نمونه

کد خود صفحه، به‌صورت دوره‌ای با بسته بودن اپلیکیشن اجرا می‌شود (wrapper تایپ‌شده روی 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;
}

نکته‌ها: در اندروید ظاهر به لانچر بستگی دارد: بسیاری عدد نشان می‌دهند، برخی فقط نقطه، برخی هیچ - اطلاعات را فقط در نشان نگذارید. در وب 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() → بولی. request() → Promise<string>: 'already_ignoring'، 'dialog_opened' (گزینه ساخت «پرسیدن مستقیم برای رد بهینه‌سازی باتری»)، 'settings_opened'، 'unavailable'، یا 'unsupported' بیرون از اپلیکیشن اندروید. نیازمندی: برای فهرست تنظیمات چیزی لازم نیست.

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'

نمونه

حسگرهای استاندارد تناسب اندام بلوتوث - ضربان قلب، سرعت/آهنگ رکاب‌زنی، توان، سرعت/آهنگ دویدن و باتری حسگر - با اعدادی که از قبل تجزیه شده‌اند، در اپلیکیشن اندروید.

مقدار برگشتی: هر متد Promise<void> برمی‌گرداند (setWheelCircumference مقدار void برمی‌گرداند). همهٔ موارد دیگر به‌صورت رویدادهای window با نام appmint:ble می‌رسند، با detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. نیازمندی: بلوتوث، که وقتی کد از 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>;
}

نکته‌ها: هر اسکن ۱۵ ثانیه طول می‌کشد (stopScan() زودتر تمامش می‌کند)؛ هر بار یک حسگر وصل است. کدهای خطا: not_enabled، permission_denied، bluetooth_unavailable، bluetooth_off، scan_failed، invalid_address. سرعت به دو بسته و اندازه چرخ نیاز دارد (پیش‌فرض ۲۰۹۶ میلی‌متر). همین پیام‌ها به window.onAppMintBle هم می‌روند؛ گوش دادن به رویداد ساده‌تر است. برای هر دستگاه بلوتوث دیگر از navigator.bluetooth.

appmintNative.brightness appmintNative#

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

نمونه

روشنایی صفحه را برای کارت پرواز یا کد QR بالا می‌برد، سپس کنترل را به سیستم برمی‌گرداند. فقط بومی - صفحهٔ وب نمی‌تواند نور پس‌زمینه را کنترل کند.

مقدار برگشتی: get() ← Promise<number | null> (0 تا 1 وقتی اپلیکیشن سطح را override می‌کند؛ -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() بازنویسی پنجره را می‌خواند، پس تا زمانی که set() را فراخوانی نکنید -1 است - سطح سیستم نیست.

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 اپلیکیشن استفاده می‌کند. این تب یک مرورگر جداست و کوکی‌ها یا localStorage اپلیکیشن را به اشتراک نمی‌گذارد. close() تب را از داخل کد اپلیکیشن می‌بندد؛ در وب کاری نمی‌کند. onClosed در وب هرگز فعال نمی‌شود.

appmintNative.calendar appmintNative#

calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar

نمونه

رویدادها را در تقویم گوشی خود کاربر (ارائه‌دهنده تقویم اندروید) اضافه، فهرست و حذف می‌کند و اپلیکیشن تقویم را در یک تاریخ باز می‌کند.

مقدار برگشتی: requestPermission() ← Promise<'granted' | 'denied'>. createEvent(input) ← Promise<string | null> (شناسه رویداد؛ null وقتی کاربر دسترسی تقویم را رد کند - وقتی خود رویداد ساخته نشود رد می‌شود، مثلاً تاریخ نامعتبر). listEvents({ from, to }) ← Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (زمان‌ها به epoch ms). deleteEvent(id) / openCalendar(date?) ← Promise<boolean>. فقط createEvent رد می‌شود، آن هم فقط برای رویدادی که ساخته نشد. نیازمندی: هنگام ساخت Calendar را در مرحله ۴ (Access) روشن کنید - وقتی 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?)

نمونه

با دوربین عکس می‌گیرد یا از گالری عکس انتخاب می‌کند: افزونه Camera در Capacitor در اپلیکیشن، ورودی فایل با capture در وب.

مقدار برگشتی: take() ← Promise<Photo | null> با { dataUrl, mime }؛ null در لغو یا مجوز ردشده. pick() ← Promise<Photo[]>، در لغو خالی. نیازمندی: دوربین در مرحله ۴ (دسترسی) - وقتی کد شما صدا بزند ساخت آن را روشن می‌کند 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 / [] برمی‌گرداند و خطا نمی‌اندازد - یک راهنمای کوتاه نشان دهید تا کاربر بفهمد چرا چیزی نشد. دوربین حساس است: آن را در فرم Data safety در 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' هستند.
  • برخی ویژگی‌ها فقط وقتی وجود دارند که ساخت آن‌ها را روشن کرده باشد (دسترسی به پوشه، کلیدهای امن، کنترل رسانه، پیامک، مخاطبان). از can() بپرسید، نه isNativeApp().
  • یک نام ناشناخته 'none' برمی‌گرداند. برای یک بله/خیر ساده از این استفاده کنید has(cap).

appmintNative.clearPushInbox appmintNative#

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

نمونه

صندوق ورودی AppMint Push را روی این گوشی خالی می‌کند. دائمی است - واگرد ندارد.

مقدار برگشتی: boolean همزمان - true وقتی پاک شد؛ در وب همیشه false. نیازمندی: Android: AppMint Push در مرحلهٔ یکپارچه‌سازی (مرحله ۳) هنگام ساخت روشن باشد.

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.clearShared appmintNative#

clearShared(): void

نمونه

متن یا پیوندی را که اپلیکیشن دیگری به این اپلیکیشن فرستاده فراموش می‌کند، وقتی اپلیکیشن شما آن را ذخیره کرد.

مقدار بازگشتی: هیچ. نیازمندی: هیچ (در وب کاری نمی‌کند).

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

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

اشتراک را فقط پس از ذخیره پاک کنید، و وقتی ذخیره شکست خورد نگه دارید:

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

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

اشتراک‌گذاری‌ای را که هنگام باز بودن اپلیکیشن می‌رسد مدیریت کنید:

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

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

نکته‌ها: sharedText() عمداً اشتراک را نگه می‌دارد تا این فراخوانی شود، تا صفحه‌ای که دیر سوار می‌شود هم آن را ببیند. clearShared() را فقط بعد از اینکه اشتراک با امنیت ذخیره شد فراخوانی کنید - اگر ذخیره شکست خورد آن را بگذارید بماند تا کاربر از دست ندهد.

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
}

آن را از یک ردیف فهرست همراه با toast پخش کنید:

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

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

نکته‌ها: با پیروی از تنظیم گوشی (توصیه‌شده) وقتی کاربر «صداهای لمس» را در گوشی خاموش کرده کلیک بی‌صدا است. با همیشه در هر ضربه پخش شود، هر ضربه از قبل کلیک می‌دهد، پس فراخوانی‌های اضافی click() لازم نیست. فقط اندروید (پوسته 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
  }
}

یک کد را کپی کنید و با یک toast تأیید کنید:

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 شما می‌تواند استفاده کند.

مقدار برگشتی: یک تابع لغو اشتراک که متغیر و attribute را هم حذف می‌کند. نیازمندی: هیچ.

این --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 خود window است (صفحه‌ای که یکجا اسکرول می‌شود)؛ مقدار پیش‌فرض range برابر 64 px است.
  • شنونده اسکرول passive است و در هر فریم انیمیشن یک بار به‌روز می‌شود، پس اسکرول را کند نمی‌کند.

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

یک hook قابل استفادهٔ مجدد که وقتی عددی نیست دکمه را پنهان می‌کند.

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( وقتی می‌سازید پیامک (خواندن و ارسال) را روشن نمی‌کند، پس مجوز محدودشده 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');
}

یک عکس را از دوربین کپی کنید، سپس با یک toast تأیید کنید.

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

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

دکمه را فقط جایی نشان دهید که کلیپ‌بورد تصویر می‌پذیرد، و یک تصویر را از 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 در همه‌جا امن‌ترین نوع است؛ اپلیکیشن اندروید JPEG و WebP را هم می‌پذیرد.

appmintNative.Device appmintNative#

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

نمونه

شکل @capacitor/device: مدل گوشی، نسخهٔ سیستم‌عامل، یک شناسهٔ پایدار و زبان. در اپلیکیشن از افزونهٔ 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' : '');
}

یک شناسه پایدار و کد زبان.

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() در وب شناسه localStorage از installId() است و وقتی حافظه مسدود باشد خطا می‌دهد.

appmintNative.deviceInfo appmintNative#

deviceInfo(): DeviceInfo

نمونه

به‌صورت همزمان می‌گوید اپلیکیشن کجا اجرا می‌شود (platform) و زبان، به‌علاوه گزارش کامل دستگاه پوسته اندروید داخل اپلیکیشن اندروید.

مقدار بازگشتی: DeviceInfo بلافاصله: همیشه { platform, language } - platform در اپلیکیشن 'android' و در مرورگر 'web' است. در اپلیکیشن اندروید 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);
}

هشدار و پرسش:

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

نکته‌ها: بستن یک تأیید { value: false } می‌دهد؛ بستن یک پرسش { value: '', cancelled: true } می‌دهد. کادرهای گفت‌وگو صفحه را مسدود می‌کنند - برای تصمیم‌های واقعی از آن‌ها استفاده کنید و برای پیام‌هایی مثل «ذخیره شد» از toast(). prompt عنوان دکمه‌های سفارشی را به افزونه نمی‌دهد.

appmintNative.dismissPushMessage appmintNative#

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

نمونه

یک پیام را از صندوق ورودی AppMint Push پنهان می‌کند (با کشیدن). واگرد آن با restorePushMessage(id).

مقدار بازگشتی: boolean به‌صورت همزمان - true اگر پنهان بود. در وب همیشه false. نیازمندی: اندروید: 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>

نمونه

اسکنر سند سیستم را باز می‌کند - تشخیص لبه، صاف کردن، چند صفحه - اسکنر ML Kit گوگل در اپلیکیشن اندروید. صفحه‌ها به‌صورت URL داده JPEG برمی‌گردند.

مقدار برگشتی: Promise<{ pages: string[]; pdfPath?: string } | null> - null وقتی کاربر لغو کند، اسکنر شروع نشود، یا در وب. isSupported() ← Promise<boolean>: آیا این گوشی می‌تواند اسکن کند (Google Play services)؛ در وب false. نیازمندی: چیزی برای روشن کردن نیست (در اندروید اسکنر در Google Play services اجرا می‌شود و به مجوز دوربین نیاز ندارد).

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، تصویر، فایل office) را در اپلیکیشن نمایشگر گوشی باز می‌کند. در وب نشانی را در تب جدید باز می‌کند.

مقدار بازگشتی: Promise<boolean> - true وقتی نمایشگر باز شد، false وقتی ناموفق بود (اپلیکیشنی برای آن نوع نیست، مسیر خراب) یا هدف در این بستر باز نمی‌شود. نیازمندی: چیزی برای روشن‌کردن نیست؛ از افزونه FileViewer در Capacitor که در اپلیکیشن هست استفاده می‌کند.

یک سند را از وب باز کنید:

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. نیازمندی: اپلیکیشن ساخته‌شده با هوش مصنوعی وقتی کد از folder استفاده کند خودش گزینهٔ دسترسی به پوشهٔ بومی (SAF) را روشن می‌کند؛ در ساخت دستی آن را در مرحله ۴ (دسترسی) تیک بزنید.

وصل شوید، پوشه را نشان دهید و قطع کنید:

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'

نمونه

Geofenceهایی که با بسته بودن اپلیکیشن فعال می‌شوند و از راه‌اندازی مجدد جان به در می‌برند: add, remove, list, events, clearEvents, onEvent, available.

مقدار برگشتی: add(spec) ← Promise<BgResult> که وقتی Play services آن را پذیرفت یا رد کرد حل می‌شود ({ id, ok, reason? } - background_permission_required، location_off، too_many_geofences، …). remove / clearEvents ← Promise<boolean>. list() ← Promise<GeofenceSpec[]>. events() ← Promise<GeofenceEvent[]> (۱۰۰ مورد آخر، از جمله وقتی اپلیکیشن بسته بود). onEvent(cb) ← لغو اشتراک. نیازمندی: Geofences در ساخت و «Allow all the time» (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: افزونه Geolocation در Capacitor در اپلیکیشن، navigator.geolocation استاندارد در وب. همه شیء موقعیت W3C را برمی‌گردانند.

مقدار بازگشتی: getCurrentPosition(opts) → Promise<GeolocationPosition> (در صورت رد، پایان مهلت یا نبود موقعیت رد می‌شود)؛ watchPosition(opts, cb) → Promise<string> (شناسه نظارت)؛ clearWatch({ id }) → Promise<void>؛ checkPermissions() / requestPermissions() → Promise<{ location, coarseLocation }> (هر کدام 'granted' | 'denied' | 'prompt'؛ در اندروید 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?';
  }
}

یک مسیر را دنبال کنید، سپس متوقف کنید. callback مقدار 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 را می‌خواهید از این استفاده کنید. در اندروید 12 به بالا ممکن است کاربر فقط موقعیت تقریبی را مجاز کند - همچنان موقعیت می‌گیرید، فقط کم‌دقت‌تر. در وب requestPermissions() یک درخواست موقعیت واقعی می‌فرستد تا پنجره مرورگر ظاهر شود. موقعیت داده شخصی است: آن را در فرم ایمنی داده‌های 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' }؛ {} وقتی نیستند و در وب. نیازمندی: اندروید: AppMint Push باید هنگام ساخت در مرحله Integrate (مرحله ۳) روشن شده باشد.

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

نکته‌ها: این همان چیزی است که گوشی نگه می‌دارد؛ مرکز ارسال آن را پس از check-in بعدی نصب می‌بیند.

appmintNative.getPushUser appmintNative#

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

نمونه

شناسه کاربری‌ای را می‌خواند که این نصب آخرین بار به AppMint Push داده است با setPushUser.

مقدار بازگشتی: string به‌صورت همزمان - شناسه ذخیره‌شده، یا '' وقتی خارج‌شده، هرگز تنظیم‌نشده، یا در وب. نیازمندی: اندروید: 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 را با شناسهٔ حساب صدا بزنید و هنگام خروج با ''؛ وقتی شناسه رد شود یا AppMint Push خاموش باشد false برمی‌گرداند:

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

شناسه ذخیره‌شده را در یک هوک 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? } (ms، ms، 0 تا 1، 0 تا 1):

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

await hapticPlay([
  { at: 0,   intensity: 0.4 },               // soft tap
  { at: 150, intensity: 0.4 },               // soft tap
  { at: 320, duration: 120, intensity: 1 },  // strong buzz
]);

آن را با 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 برمی‌گرداند (نه Promise؛ await روی آن بی‌ضرر است). نیازمندی: Vibrate در مرحله ۴ (Access)؛ ساخت‌های هوش مصنوعی وقتی کد از آن استفاده کند آن را برایتان روشن می‌کنند 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 کاری نمی‌کند. در اپلیکیشن کار می‌کند (افزونه Haptics در Capacitor)؛ در وب در صورت وجود از 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() را می‌گیرد (فهرست کامل را در آن مدخل ببینید). وقتی لازم است بدانید کجا اجرا می‌شود ('native' یا 'web') از can() استفاده کنید، مثلاً برای تغییر یک برچسب.

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 می‌خواهد صفحه‌کلید را پنهان کند. در وب فیلد فوکوس‌شده را blur می‌کند، که همان چیزی است که در مرورگر صفحه‌کلید را می‌بندد. صدا زدن آن وقتی صفحه‌کلیدی باز نیست بی‌خطر است.

appmintNative.inAppCampaigns appmintNative#

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

نمونه

پیام‌های درون‌برنامه‌ای که این نصب همین حالا دارد، به‌صورت {id, name}[]: کمپین‌های سازنده که این کاربر در مخاطبانشان است.

مقدار برگشتی: { id: string; name: string }[] همزمان؛ در وب و بدون AppMint Push []. نیازمندی: Android: AppMint Push در مرحلهٔ یکپارچه‌سازی (مرحله ۳) هنگام ساخت روشن باشد.

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

نمونه

یک شناسه تصادفی ثابت برای این نصب می‌دهد: شناسه خود پوسته در اپلیکیشن اندروید، یک شناسه تصادفی ذخیره‌شده در 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>;
}

نکته‌ها: در به‌روزرسانی‌های اپلیکیشن ثابت می‌ماند و با حذف نصب یا «پاک‌کردن داده» تغییر می‌کند. شناسه سخت‌افزار نیست. در وب وقتی localStorage مسدود باشد عمداً خطا می‌دهد - یک شناسه خالی مشترک کاربران را قاطی می‌کند.

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 در مرورگر. نیازمندی: هیچ.

یک نشان «اپلیکیشن اندروید» و در مرورگر یک پیوند دانلود نشان دهید.

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() → بولی. 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();
}

گوش دهید (هنگام unmount handle را حذف کنید):

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 است (رویداد show همچنان فعال می‌شود، و با تغییر عدد دوباره فعال می‌شود). رویدادهای hide فقط وقتی صفحه‌کلید واقعاً بسته شود فعال می‌شوند، پس می‌توانند «آیا صفحه‌کلید باز است» را دنبال کنند. رویدادهای «Will» و «did» با هم فعال می‌شوند. فقط برای padding، onKeyboard((px) => …) ساده‌تر است.

appmintNative.launchUrl appmintNative#

launchUrl(): Promise<string>

نمونه

پیوندی را که اپلیکیشن را راه انداخته یا از سر گرفته برمی‌گرداند - پیوند عمیق، میان‌بر آیکون، ضربهٔ ویجت، اعلان - تا اپلیکیشن بتواند صفحهٔ درست را باز کند.

مقدار بازگشتی: 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 مسیر را در path خود دارد.

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

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

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

نکته‌ها:

  • مقدار نگه داشته می‌شود، مصرف نمی‌شود: کامپوننتی که دیر mount می‌شود هم آن را می‌بیند. پیوند جدیدتر آن را جایگزین می‌کند.
  • از پوسته خوانده می‌شود (WebToApk.getLaunchUrl())؛ هرگز در اپلیکیشن ساخته‌شده با هوش مصنوعی مستقیماً آن را صدا نزنید. میان‌برها و ضربه‌های ویجت به‌صورت می‌رسند appmint-shortcut://<package><route>.
  • با new URL(u) تجزیه کنید و به path آن بروید؛ خود 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 میلی‌ثانیهٔ epoch است، duration ثانیه است. وقتی تاریخچهٔ تماس خاموش است یا مجوز رد شده خالی است. نیازمندی: گزینهٔ تاریخچهٔ تماس (خواندن) در مرحله ۴ (دسترسی) - وقتی کد شما این را صدا بزند ساخت آن را روشن می‌کند - و مجوز کاربر در اولین استفاده.

تماس‌های بی‌پاسخ 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 در اپلیکیشن به ۲۰۰۰ محدود می‌شود. فهرست خالی نمی‌تواند «بدون تماس» را از «مجوز رد شده» تشخیص دهد. can('callLog') در وب 'none' است. سابقه تماس یک مجوز محدودشده Google Play است: به یک Permissions Declaration تأییدشده در Play Console نیاز دارید، وگرنه اپلیکیشن رد می‌شود.

appmintNative.listContacts appmintNative#

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

نمونه

دفترچه تلفن را صفحه‌به‌صفحه می‌خواند. فقط در اپلیکیشن اندروید و از طریق پل پوسته.

مقدار برگشتی: Promise<Contact[]>. آرایه خالی وقتی مخاطبی نیست، دسترسی رد شده، یا مخاطبان هنگام ساخت فعال نشده بود. نیازمندی: کلید Contacts در مرحله ۴ (Access) - وقتی کد شما این را فراخوانی می‌کند ساخت آن را روشن می‌کند - و مجوز کاربر در اولین استفاده.

یک صفحه فهرست مخاطبان. آن را فقط جایی نمایش دهید که می‌تواند کار کند. هر مخاطب { 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 به ۲۰۰۰ محدود می‌شود. برای یک نفر از pickContact() استفاده کنید که به مجوز نیاز ندارد. مخاطبان داده شخصی است: آن را در فرم Data safety Play و سیاست حفظ حریم خصوصی خود اعلام کنید.

appmintNative.listSms appmintNative#

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

نمونه

پیامک‌ها را از صندوق ورودی، ارسال‌شده یا پیش‌نویس‌ها می‌خواند، جدیدترین اول. فقط اپلیکیشن Android.

مقدار برگشتی: Promise<SmsMessage[]> - هر کدام { id, address, body, date, read, box }. وقتی پیامک خاموش است، مجوز رد شده، یا صندوق خالی است خالی. نیازمندی: کلید SMS (read & send) در مرحله ۴ (Access) - وقتی کد شما این را فراخوانی می‌کند ساخت آن را روشن می‌کند - و مجوز کاربر در اولین استفاده.

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' است. پیام‌های ورودی جدید جزو این wrapper نیستند؛ صفحه‌های دست‌نویس آن‌ها را از رویداد appmint:sms-received می‌گیرند (برای پل خام listSms را ببینید). پیامک یک مجوز محدودشدهٔ Google Play است: به Permissions Declaration تأییدشده در Play Console نیاز دارید که معمولاً فقط به اپلیکیشن‌های پیش‌فرض پیامک داده می‌شود.

appmintNative.liveProgress appmintNative#

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

نمونه

یک کارت وضعیت چسبنده و بی‌صدا در پرده اعلان اندروید که در جا به‌روز می‌کنید - تحویل، آپلود، تایمر، ورزش. start, update, end.

مقدار بازگشتی: start / update → Promise<boolean> (false وقتی در دسترس نیست، وقتی اعلان‌ها مجاز نیستند یا وقتی کاربر دسته «وضعیت زنده» آن را خاموش کرده)؛ end → Promise<void>. نیازمندی: اپلیکیشن اندروید، و مجوز اعلان در اندروید 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: اعلان‌ها در مرحلهٔ دسترسی (مرحله ۴) هنگام ساخت روشن باشد.

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

نکته‌ها:

  • در اپلیکیشن افزونه ScreenOrientation در Capacitor قفل را اعمال می‌کند.
  • در وب از screen.orientation.lock() استفاده می‌کند. بیشتر مرورگرها آن را بیرون از تمام‌صفحه رد می‌کنند و مرورگرهای دسکتاپ همیشه رد می‌کنند، پس آنجا انتظار false داشته باشید.
  • همیشه به کاربر راه خروج از یک نمای قفل‌شده بدهید.

appmintNative.longPress appmintNative#

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

نمونه

وقتی کاربر انگشت را روی یک عنصر نگه می‌دارد شما را صدا می‌زند - برای منوی زمینه یک ردیف، واکنش‌ها یا اقدام‌های کارت.

مقدار بازگشتی: یک تابع لغو اشتراک - هنگام unmount آن را فراخوانی کنید. نیازمندی: هیچ.

گزینه‌ها: 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 پیکسل آن را لغو می‌کند، بنابراین اسکرول هرگز آن را فعال نمی‌کند. همچنین منوی لمس طولانی خود مرورگر را روی آن عنصر مسدود می‌کند.
  • لمس طولانی دیده نمی‌شود. همیشه برای همان اقدام یک دکمه قابل‌مشاهده نگه دارید.

appmintNative.markPushRead appmintNative#

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

نمونه

یک پیام صندوق ورودی AppMint Push را خوانده‌شده علامت می‌زند، یا اگر بدون آرگومان فراخوانی شود همه پیام‌ها را.

مقدار برگشتی: boolean به‌صورت همزمان - true اگر چیزی تغییر کرده باشد. در وب همیشه false. نیازمندی: اندروید: AppMint Push باید هنگام ساخت در مرحله Integrate (مرحله ۳) روشن شده باشد.

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 وقتی چیزی قابل ارائه نبود). نیازمندی: Media Notification در مرحله ۴ (Access) هنگام ساخت؛ ساخت‌های هوش مصنوعی وقتی کد از آن استفاده کند آن را برایتان روشن می‌کنند 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);
}

نکته‌ها:

  • در اندروید دکمه‌ها از طریق navigator.mediaSession اپلیکیشن می‌آیند که وقتی بارگذاری صفحه تمام می‌شود نصب می‌شود. onAction ثبت‌شده زودتر (مثلاً در یک effect سوار شدن) از دست می‌رود، پس در اولین پخش ثبتش کنید.
  • کنش seek (با positionMs) فقط در وب تحویل داده می‌شود؛ اعلان Android قابلیت seek ندارد.
  • artworkUrl باید یک نشانی کامل https:// در اینترنت باشد. در Android، cast(url) گزینه‌گر «Cast به...» را باز می‌کند (یک واگذاری؛ وقتی گزینه‌گر خواسته شد 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 } callback می‌کند؛ '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 };
}

نکته‌ها: بعضی مرورگرها ابتدا دسترسی به حرکت را می‌خواهند، پس آنجا از یک ضربه شروع کنید؛ اگر کاربر رد کند handle برگردانده می‌شود اما رویدادی نمی‌رسد. can('motion') جایی که مرورگر DeviceMotionEvent ندارد 'none' است. مقادیر سنسور گمشده به‌صورت 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));
}

نکته‌ها: فقط اپلیکیشن اندروید؛ وب -1 پاسخ می‌دهد. سطحی که با setMusicVolume تنظیم شده برای اجرای بعدی نگه داشته می‌شود؛ تا وقتی صفحه سطحی تنظیم کند، همان سطح ساخت است.

appmintNative.navEnd appmintNative#

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

نمونه

همراه با appmintNative.navStart مستند شده - نمونهٔ آنجا این یکی را هم نشان می‌دهد.

navStart() نوار بارگذاری باریک بالای اپلیکیشن را برای تغییر صفحه‌ای که خود اپلیکیشن نمی‌تواند ببیند نشان می‌دهد؛ navEnd() آن را تمام می‌کند.

مقدار برگشتی: هیچ (هر دو همزمان‌اند). نیازمندی: هیچ. در وب (بدون نوار پوستهٔ اپلیکیشن) هر دو کاری نمی‌کنند.

فقط برای انتظاری از آن‌ها استفاده کنید که تا رسیدن داده چیزی در DOM تغییر نمی‌کند، و همیشه آن‌ها را با همراه کنید try/finally:

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

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

ارسالی را که مدتی طول می‌کشد بپوشانید تا کاربر هنگام کار سرور نوار را ببیند.

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

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

یک تابع کمکی کوچک بسازید که هر promise را بپوشاند.

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

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

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

نکته‌ها: آن‌ها WebToApk.navStarted() / navSettled() را صدا می‌زنند. وقتی اپلیکیشن شما صفحه‌ها را در DOM عوض می‌کند، اپلیکیشن از قبل نوار را هدایت می‌کند، پس بیشتر تغییر مسیرها به چیزی نیاز ندارند. برای تغییری کوتاه‌تر از حدود 200 ms چیزی رسم نمی‌شود، و نواری که هرگز تمام نشود بعد از 2.5 ثانیه خودش متوقف می‌شود. چرخان یا skeleton خودتان را همچنان رندر کنید؛ نوار اضافه است.

appmintNative.navStart appmintNative#

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

نمونه

navStart() نوار بارگذاری باریک بالای اپلیکیشن را برای تغییر صفحه‌ای که خود اپلیکیشن نمی‌تواند ببیند نشان می‌دهد؛ navEnd() آن را تمام می‌کند.

مقدار برگشتی: هیچ (هر دو همزمان‌اند). نیازمندی: هیچ. در وب (بدون نوار پوستهٔ اپلیکیشن) هر دو کاری نمی‌کنند.

فقط برای انتظاری از آن‌ها استفاده کنید که تا رسیدن داده چیزی در DOM تغییر نمی‌کند، و همیشه آن‌ها را با همراه کنید try/finally:

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

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

ارسالی را که مدتی طول می‌کشد بپوشانید تا کاربر هنگام کار سرور نوار را ببیند.

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

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

یک تابع کمکی کوچک بسازید که هر promise را بپوشاند.

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

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

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

نکته‌ها: آن‌ها WebToApk.navStarted() / navSettled() را صدا می‌زنند. وقتی اپلیکیشن شما صفحه‌ها را در DOM عوض می‌کند، اپلیکیشن از قبل نوار را هدایت می‌کند، پس بیشتر تغییر مسیرها به چیزی نیاز ندارند. برای تغییری کوتاه‌تر از حدود 200 ms چیزی رسم نمی‌شود، و نواری که هرگز تمام نشود بعد از 2.5 ثانیه خودش متوقف می‌شود. چرخان یا skeleton خودتان را همچنان رندر کنید؛ نوار اضافه است.

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): بررسی پشتیبانی، خواندن تگ‌ها، نوشتن یک تگ متنی. اپلیکیشن اندروید از NDEFReader خودش استفاده می‌کند؛ در وب همان Web NFC خود Chrome است (Chrome روی اندروید).

مقدار بازگشتی: isSupported(): Promise<boolean>؛ scan(onTag): Promise<() => Promise<void>> (تابع توقف)، و onTag برای هر تگ Array<{recordType, text, mediaType?}> می‌گیرد - نام‌های نوع Web NFC ('text'، 'url'، 'mime'، …) و متن رمزگشایی‌شده در همه بسترها؛ write(text): Promise<boolean> (true وقتی تگ نوشته شد، false در هر شکست). نیازمندی: در اندروید، پشتیبانی 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);
  });
}

نکته‌ها: در اندروید isSupported() هر وقت ساخت NFC داشته باشد true است - نمی‌گوید NFC روشن است یا نه (window.AndroidNFC.startScan() می‌گوید). بعد از تابع توقف، onTag دیگر فراخوانی نمی‌شود. در اندروید خود اپلیکیشن تا وقتی در جلو است تگ‌ها را می‌گیرد (تا یک ضربه اپلیکیشن دیگری را باز نکند)، فقط آن‌ها را به شما نمی‌دهد. text یک رکورد متنی پیشوند زبان ندارد؛ text یک رکورد URL همان URL کامل است. write() منتظر می‌ماند تا تگی لمس شود. برای نوشتن رکورد URL یا mime و قفل کردن تگ مستقیم از NDEFReader استفاده کنید.

appmintNative.notify appmintNative#

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

نمونه

یک اعلان را همین حالا از یک اپلیکیشن ساخته‌شده با هوش مصنوعی نشان می‌دهد، در اپلیکیشن اندروید و در وب.

مقدار برگشتی: Promise<boolean> - true وقتی ارسال شد، false وقتی کاربر مجوز اعلان را رد کرد، اعلان‌ها برای اپلیکیشن خاموش‌اند، یا اینجا در دسترس نیستند. بار اول خودش مجوز را می‌خواهد (Android 13 و بالاتر و وب). نیازمندی: Android: اعلان‌ها در مرحلهٔ دسترسی (مرحله ۴) هنگام ساخت روشن باشد.

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

نکته‌ها: اندروید از پل اعلان پوسته استفاده می‌کند (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: اعلان‌ها در مرحلهٔ دسترسی (مرحله ۴) هنگام ساخت روشن باشد، و مجوز کاربر در 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 همیشه '' است. ضربه فقط وقتی اپلیکیشن در حال اجراست به صفحه می‌رسد. در وب wrapper یک اعلان ساده بدون دکمه می‌فرستد، به همین دلیل مثال ابتدا can() را بررسی می‌کند.

appmintNative.offlineBanner appmintNative#

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

نمونه

یک نوار آماده را نصب می‌کند که وقتی اتصال قطع شود زیر نوار وضعیت می‌لغزد، و وقتی برگشت مختصراً «دوباره آنلاین» می‌گوید.

مقدار برگشتی: یک تابع لغو اشتراک که بنر را حذف می‌کند و گوش دادن را متوقف می‌کند. نیازمندی: هیچ.

آن را یک بار در شروع اپلیکیشن mount کنید:

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

بنر را فقط برای بخش واردشده اپلیکیشن mount کنید و هنگام خروج از حساب حذفش کنید.

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

نکته‌ها:

  • پیش‌فرض‌ها: «شما آفلاین هستید» و «دوباره آنلاین شدید». پیام آنلاین بعد از حدود ۱٫۸ ثانیه پنهان می‌شود؛ پیام آفلاین تا برگشتن اتصال می‌ماند.
  • اگر دستگاه هنگام mount شدن از قبل آفلاین باشد، بنر بلافاصله نمایش داده می‌شود.
  • روی onNetworkChange() ساخته شده، پس روی هر سطحی واقعی است. رنگ‌ها از توکن‌های اپلیکیشن می‌آیند (--ink / --bg آفلاین، --accent / --accent-ink آنلاین). برای صفحه‌خوان‌ها role="status" دارد.
  • آن را یک بار mount کنید. دو فراخوانی دو بنر می‌کشد.

appmintNative.onAlarm appmintNative#

onAlarm(cb) — @/lib/appmintNative

نمونه

به نتایج آلارم گوش می‌دهد: اپلیکیشنی که با آلارم باز شد (action: 'open'، با ringing)، و نحوه پایان زنگ - dismiss، snooze، missed (تا آخر زنگ خورد، یا آلارم دیگری جایش را گرفت) یا error. نتایج مربوط به زمانی که اپلیکیشن بسته بود وقتی صفحه بعدی بارگذاری شد تحویل داده می‌شوند.

مقدار برگشتی: تابعی که گوش دادن را متوقف می‌کند. کال‌بک { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? } می‌گیرد. نیازمندی: 🔔 Alarm clock در مرحله Access (مرحله ۴) هنگام ساخت.

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

نمونه

به یک برگه، modal، lightbox یا مرحلهٔ wizard باز اجازه می‌دهد ابتدا به اشارهٔ برگشت پاسخ دهد - کشیدن از لبهٔ Android / دکمهٔ برگشت در اپلیکیشن، دکمهٔ برگشت مرورگر در وب.

مقدار برگشتی: یک تابع لغو اشتراک - وقتی نما بسته یا unmount شد صدا بزنید. کنترل‌کنندهٔ شما 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.onInAppMessage appmintNative#

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

نمونه

برای هر پیام درون‌برنامه‌ای (کمپین‌های سازنده) که روی این نصب نشان داده، کلیک، رد یا پاسخ داده می‌شود شما را صدا می‌زند. تابع لغو اشتراک را برمی‌گرداند.

مقدار بازگشتی: () => void. callback {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?} را می‌گیرد. در وب هرگز فعال نمی‌شود. نیازمندی: اندروید: 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

نمونه

هر بار که تغییر کند به شما می‌گوید صفحه‌کلید روی صفحه چند پیکسل از صفحه را می‌پوشاند، تا یک نوار پایینی بتواند بالای آن بماند.

مقدار برگشتی: یک تابع لغو اشتراک - هنگام unmount فراخوانی کنید. کال‌بک یک عدد به پیکسل 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>;
}

یک hook قابل استفادهٔ مجدد، با فاصلهٔ ناحیهٔ امن که هنگام باز بودن صفحه‌کلید ناپدید می‌شود:

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 استفاده می‌کند، callback را یک بار بلافاصله صدا می‌زند، و کمتر از 80 px را ظاهر مرورگر حساب می‌کند (گزارش می‌شود به‌صورت 0).
  • در اپلیکیشن اولین فراخوانی وقتی می‌آید که صفحه‌کلید اول بار باز می‌شود، پس state خود را از آن شروع کنید 0.

appmintNative.onNetworkChange appmintNative#

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

نمونه

هر وقت اتصال قطع یا برقرار شود شما را صدا می‌زند - برای بنر آفلاین، یا تلاش دوبارهٔ نوشتن‌های صف‌شده.

مقدار بازگشتی: یک تابع لغو اشتراک - هنگام unmount آن را فراخوانی کنید. نیازمندی: هیچ.

کار در صف را وقتی دستگاه دوباره آنلاین شد دوباره امتحان کنید:

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

وضعیت فعلی را بلافاصله نشان دهید، سپس تغییرات را دنبال کنید (callback هنگام اشتراک صدا زده نمی‌شود):

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() را بخوانید.
  • در اپلیکیشن به افزونه Network در Capacitor گوش می‌دهد (networkStatusChange)؛ type یکی از 'wifi'، 'cellular'، 'none' یا 'unknown'.
  • در وب به رویدادهای online / offline گوش می‌دهد و گزارش می‌کند networkStatus().
  • برای یک بنر آماده از offlineBanner() استفاده کنید که روی این ساخته شده است.

appmintNative.onNotificationAction appmintNative#

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

نمونه

به ضربه‌ها روی دکمه‌های اعلان گوش می‌دهد (از notifyWithActions).

مقدار برگشتی: تابعی که گوش دادن را متوقف می‌کند. کال‌بک { actionId, tag } می‌گیرد - شناسه دکمه و تگ اعلان. اپلیکیشن اندروید 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]);
}

رویداد خام در صفحه دست‌نویس (اپلیکیشن اندروید):

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

نکته‌ها: در اپلیکیشن اندروید این رویداد appmint:notification-action است (همچنین window.onAppMintNotificationAction(detail))؛ در وب کاری نمی‌کند. ضربه فقط وقتی تحویل داده می‌شود که اپلیکیشن در حال اجراست - لمس دکمه اپلیکیشن را باز نمی‌کند.

appmintNative.onNotificationClick appmintNative#

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

نمونه

وقتی کاربر روی اعلانی که اپلیکیشن ارسال کرده ضربه می‌زند اجرا می‌شود - از جمله ضربه‌ای که اپلیکیشن را از حالت بسته باز کرده - تا اپلیکیشن صفحه درست را باز کند.

مقدار برگشتی: تابع لغو اشتراک. callback مقدار { 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 KB) و به یک نیاز دارد tag.

appmintNative.onPullToRefresh appmintNative#

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

نمونه

حرکت کشیدن-به-پایین-برای-تازه‌سازی را به یک فهرست اسکرولی اضافه می‌کند: در بالا بکشید، رها کنید و داده‌هایتان دوباره بارگذاری می‌شود.

مقدار بازگشتی: یک تابع لغو اشتراک - هنگام unmount آن را فراخوانی کنید. نیازمندی: هیچ.

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

یک فهرست را از طریق data hook تازه‌سازی کنید، با یک لرزش ظریف وقتی داده رسید:

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.onShared appmintNative#

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

نمونه

وقتی اپلیکیشن دیگری متن یا پیوندی را به این اپلیکیشن که از قبل باز است به اشتراک می‌گذارد، شما را صدا می‌زند.

مقدار برگشتی: یک تابع لغو اشتراک - هنگام unmount صدا بزنید. نیازمندی: گزینهٔ ساخت دریافت اشتراک از اپلیکیشن‌های دیگر با «متن و پیوندها» - وقتی کد شما onShared( یا sharedText(.

هر دو حالت را مدیریت کنید - اشتراکی که اپلیکیشن را راه‌اندازی کرد و اشتراکی که بعداً می‌رسد:

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

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

یک صفحه دست‌نویس می‌تواند خودش به رویداد پوسته گوش دهد:

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

یک فرم «یادداشت جدید» را از یک پیوند اشتراکی پر کنید، فقط جایی که ساخت می‌تواند اشتراک دریافت کند:

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

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

نکته‌ها:

  • اندروید: به رویداد appmint:shared پوسته گوش می‌دهد؛ پوسته هنگام تعریف توسط صفحه window.onAppMintShared(detail) را هم فراخوانی می‌کند. همان بار برای sharedText() هم تا نگه داشته می‌شود clearShared().
  • در وب هرگز فعال نمی‌شود و یک لغو اشتراک بی‌اثر برمی‌گرداند.

appmintNative.onVolumeChange appmintNative#

onVolumeChange(cb) — @/lib/appmintNative

نمونه

هر وقت یکی از سطوح صدای گوشی عوض شود cb را صدا می‌زند - کلیدهای سخت‌افزاری، نوار تنظیم سیستم، اپلیکیشن دیگر.

مقدار برگشتی: تابعی که گوش دادن را متوقف می‌کند (آخرین توقف‌کننده، نظارت بومی را خاموش می‌کند). callback برای هر جریانی که تغییر کرده { 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>;
}

وقتی صدای رسانه هنگام پخش ویدیو تا آخر کم شده هشدار دهید، و هنگام unmount گوش دادن را متوقف کنید:

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 اندروید استفاده می‌کند؛ در وب از navigator.credentials.

مقدار برگشتی: available() ← 'native' | 'web' | 'none'؛ create(optionsJSON) ← وعده‌ای از RegistrationResponseJSON؛ get(optionsJSON) ← وعده‌ای از AuthenticationResponseJSON (هر دو را با POST به سرور خود بفرستید). با DOMException رد می‌شوند (NotAllowedError، InvalidStateError، SecurityError، NotSupportedError). origin() ← رشته، assetLinks() ← آرایه (فقط در اپلیکیشن؛ در وب '' / []). نیازمندی: یک سرور passkey؛ در اپلیکیشن همچنین اندروید ۹+ با خدمات 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>

نمونه

انتخابگر مخاطب سیستم را باز می‌کند تا کاربر یک نفر را انتخاب کند: انتخابگر اندروید در اپلیکیشن، Contact Picker API در Chrome روی اندروید.

مقدار برگشتی: 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') در مرورگرهایی که Contact Picker API ندارند (دسکتاپ، Safari) 'none' است - آنجا دکمه را رسم نکنید.

appmintNative.pickFile appmintNative#

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

نمونه

به کاربر اجازه می‌دهد یک فایل انتخاب کند و آن را به‌صورت متن بخواند. در اپلیکیشن انتخابگر اسناد اندروید را باز می‌کند؛ در وب یک ورودی فایل معمولی باز می‌کند.

مقدار برگشتی: Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }، یا null فقط وقتی کاربر لغو کند. با یک Error رد می‌شود که code آن 'no-picker' است (گوشی اپلیکیشن انتخاب سند ندارد)، 'disabled' یا 'failed' وقتی هیچ انتخابگری باز نشد. text() کل فایل را می‌خواند و با کد پل (مثلاً 'not-found') رد می‌شود وقتی نتواند. نیازمندی: هیچ. با تیک Native Folder Access (SAF) در مرحله ۴ (Access) اپلیکیشن از انتخابگر سند اندروید استفاده می‌کند؛ بدون آن از انتخابگر فایل خود صفحه استفاده می‌کند.

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 MB تا انتها می‌خواند و به‌صورت 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> برمی‌گرداند. هیچ‌کدام رد نمی‌شوند. نیازمندی: در اپلیکیشن اندروید گزینه Picture-in-Picture (PiP) ساخت در مرحله ۳ (Integrate) - وقتی کد شما فراخوانی می‌کند خودکار روشن می‌شود 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]);
}

نکته‌ها:

  • اندروید (۸+): کل اپلیکیشن در پنجره شناور جمع می‌شود و آرگومان video نادیده گرفته می‌شود. در اندروید قدیمی‌تر یا در ساختی بدون گزینه PiP مقدار false برمی‌گرداند. کاربر پنجره را بزرگ می‌کند تا برگردد؛ exit() آنجا کاری نمی‌کند.
  • وب: تصویر در تصویر خود عنصر <video>. ویدیو باید بارگذاری شده باشد (معمولاً در حال پخش)؛ فراخوانی بدون video برمی‌گرداند false.
  • آن را از یک ضربه فراخوانی کنید.

appmintNative.platform appmintNative#

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

نمونه

به شما می‌گوید کد روی کدام بستر اجرا می‌شود: اپلیکیشن Android نصب‌شده، یا مرورگر وب.

مقدار بازگشتی: یک رشته، بلافاصله: 'android' داخل اپلیکیشن، 'web' در مرورگر. ابتدا از Capacitor می‌پرسد (Capacitor.getPlatform())، سپس پل پوسته اندروید را بررسی می‌کند، وگرنه 'web'. این نوع 'ios' را هم فهرست می‌کند، اما اپلیکیشن AppMint هرگز آن را نمی‌گیرد - AppMint فقط اپلیکیشن اندروید می‌سازد. نیازمندی: هیچ.

پیوند و متن درست را انتخاب کنید.

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>. نیازمندی: هیچ. در اپلیکیشن اندروید از مسدودکننده اسکرین‌شات پوسته استفاده می‌کند (اسکرین‌شات، ضبط صفحه، پخش و تصویر کوچک اخیر).

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 در مرحلهٔ یکپارچه‌سازی (مرحله ۳) هنگام ساخت روشن باشد.

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

نکته‌ها: صندوق ورودی حقیقت ذخیره‌شده است - هنگام mount آن را بخوانید؛ رویداد 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 اپلیکیشن اندروید هست؛ در Safari و بسیاری از مرورگرهای دسکتاپ نیست - در آن صورت برمی‌گرداند []).
  • یک <input type="file" accept="image/*"> ساده انتخابگر عکس اندروید را باز می‌کند و به مجوز حافظه نیاز ندارد.
  • وقتی formats را می‌دانید محدودش کنید: سریع‌تر است و کمتر اشتباه می‌خواند. barcodeFormats() فهرست آنچه دستگاه پشتیبانی می‌کند را می‌دهد.

appmintNative.readOtp appmintNative#

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

نمونه

منتظر کد یک‌بارمصرف پیامکی برای این اپلیکیشن می‌ماند و آن را برمی‌گرداند - شیت رضایت پیامک Google در اپلیکیشن اندروید، WebOTP در Chrome روی وب.

مقدار بازگشتی: Promise<string | null> - کد، یا null وقتی کاربر رد کرد، پنج دقیقه گذشت یا signal لغو کرد. وقتی can('otp') برابر 'none' باشد خطا می‌دهد. نیازمندی: چیزی برای روشن‌کردن نیست. پیامک باید با @<host> #<code> تمام شود (در اپلیکیشن اندروید میزبان 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 اپلیکیشن اندروید، یا 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 services مدل را بگیرد - پیشرفت را نشان دهید.

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. نیازمندی: در اپلیکیشن به چیزی نیاز نیست - به‌روزرسانی از راه دور در آنجا به‌صورت بومی انجام می‌شود.

فراخوانی هنگام شروع امن است؛ در اپلیکیشن بلافاصله برمی‌گردد:

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 است: پوسته مانیفست را به‌صورت بومی می‌خواند و خودش اعلان را رسم می‌کند، پس هرگز دو بنر نمی‌بینید.
  • start() بدون آرگومان از manifestUrl() استفاده می‌کند. ساخت AppMint آن تگ meta را اضافه نمی‌کند، پس بدون آرگومان (یا <meta name="appmint-remote-manifest" content="…"> خودتان) چیزی دریافت نمی‌شود.
  • fetch() برای انتشار متوقف‌شده یا برگردانده‌شده، مانیفستی که فهرست platforms آن شامل web نیست، قالب مانیفست جدیدتر، یا هر خطای شبکه، null برمی‌گرداند.
  • حالت تعمیر برنده است: apply() آن‌گاه کل صفحه را با متن تعمیر می‌پوشاند. اعلان ردشده تا انتشار یک announcementId جدید پنهان می‌ماند.

appmintNative.requestDndAccess appmintNative#

requestDndAccess() — @/lib/appmintNative

نمونه

دسترسی «مزاحم نشوید» را می‌خواهد که اندروید لازم می‌داند تا اپلیکیشن بتواند وقتی «مزاحم نشوید» روشن است صدای زنگ یا اعلان را تغییر دهد.

مقدار بازگشتی: '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 اگر شناسه ناشناخته است یا کنار زده نشده بود (در وب همیشه false). نیازمندی: Android: AppMint Push در مرحلهٔ یکپارچه‌سازی (مرحله ۳) هنگام ساخت روشن باشد.

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 کافی است - runtime مقادیر --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;
}

یک فهرست اسکرول‌شونده را با اندازه‌دهی padding آن از روی اعداد، از نوارها دور نگه دارید:

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 وجود داشته باشد (خیلی زود در زمان import) همه را صفر برمی‌گرداند - آن را از داخل یک effect فراخوانی کنید.
  • در مرورگر دسکتاپ معمولاً هر چهار تا 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' و در مرورگر 'web' است (فایل در آنجا دانلود می‌شود).

مقدار برگشتی: وعده URI مورد ذخیره‌شده ('' در وب). با code 'permission-denied'، 'download-failed'، 'bad-source' یا 'failed' رد می‌شود. نیازمندی: هیچ؛ در اندروید ۱۰+ مجوزی لازم نیست.

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 ویدیو بدون 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 هنگام ساخت دوربین را در مرحله ۴ (دسترسی) روشن کنید.

فقط وقتی از آن استفاده کنید که نمای دوربین باید داخل صفحه خودتان باشد؛ در غیر این صورت 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( گزینه Camera را برایتان روشن می‌کند. اگر آن را دستی خاموش کرده باشید درخواست دوربین شکست می‌خورد و می‌گیرید 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.';
}

نکته‌ها:

  • در اپلیکیشن این اسکنر کد گوگل است (Play services، از طریق 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 هشدار قدیمی‌تر را جایگزین می‌کند. نیازمندی: 🔔 ساعت زنگ‌دار در مرحلهٔ دسترسی (مرحله ۴) هنگام ساخت.

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

نکته‌ها: فقط اپلیکیشن اندروید. repeat روزانه / هفتگی / روزهای کاری / ماهانه / سالانه ساعت یکسان را در تغییرات ساعت تابستانی حفظ می‌کنند. با 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[]> - خالی وقتی چیزی مطابقت ندارد، دسترسی رد شده، یا Contacts خاموش است. نیازمندی: همان listContacts: Contacts در مرحله ۴ (Access) (وقتی کد شما این را فراخوانی می‌کند ساخت آن را روشن می‌کند)، و مجوز کاربر در اولین استفاده.

جعبه جستجو با تأخیر کوچک هنگام تایپ.

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

وقتی Contacts در دسترس نیست و وقتی چیزی مطابقت ندارد وضعیت روشن نشان دهید:

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 وبی برای خواندن دفترچه نشانی نیست. مخاطبین داده‌های شخصی‌اند: آن را در فرم Data safety در 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' (کلیدهای امن در این ساخت نیست / Web Crypto نیست)، 'bad-alias'، 'bad-payload' (base64 معتبر یا به‌اندازهٔ کافی بلند نیست)، 'too-large' (بیش از 4 MB در یک فراخوانی در 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) ← مقدار منطقی؛ list() ← string[]. generate، encrypt و decrypt با code خطای SecureKeyError می‌اندازند. نیازمندی: کلیدهای امن (رمزگذاری) در مرحله ۴ (دسترسی) هنگام ساخت.

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() را فراخوانی کنید و دوباره تلاش کنید؛ در وب خود runtime unlock() را نشان می‌دهد. هر بار یک رکورد را رمز کنید (اپلیکیشن هر فراخوانی را به ۴ مگابایت محدود می‌کند). نام‌های مستعار: حرف، رقم، _ - .، حداکثر ۶۴.

appmintNative.sendSms appmintNative#

sendSms(phoneNumber: string, message: string): Promise<boolean>

نمونه

پیامک را مستقیم و بدون صفحهٔ تأیید می‌فرستد. فقط اپلیکیشن Android.

مقدار برگشتی: Promise<boolean> - true وقتی گوشی پیام را ارسال‌شده گزارش کند، false وقتی شکست خورد، پیامک فعال نبود، یا کاربر مجوز را رد کرد. نیازمندی: گزینهٔ پیامک (خواندن و ارسال) در مرحله ۴ (دسترسی) - وقتی کد شما این را صدا بزند ساخت آن را روشن می‌کند - و مجوز کاربر در اولین استفاده.

یک هشدار بفرستید و به کاربر بگویید چه شد.

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

نکته‌ها: wrapper با اولین پاسخ تمام می‌شود که sent است (پیام از گوشی خارج شد)، نه delivered. برای گزارش تحویل از WebToApk.sendSms خام با رویداد appmint:sms استفاده کنید. can('smsSend') در وب 'none' است (هیچ مرورگری نمی‌تواند پیامک بفرستد). هر پیامک ممکن است برای کاربر هزینه داشته باشد. پیامک یک مجوز محدودشدهٔ Google Play است: به Permissions Declaration تأییدشده در Play Console نیاز دارید، پس وقتی کاربر می‌تواند خودش ارسال را بزند composeSms() را ترجیح دهید.

appmintNative.setInAppPresentation appmintNative#

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

نمونه

انتخاب می‌کند چه کسی یک پیام درون‌برنامه‌ای (کمپین‌های سازنده) را می‌کشد: اپلیکیشن ('auto'، پیش‌فرض: طراحی بنر، کارت، شیت یا تمام‌صفحه سازنده) یا رابط خودتان ('none'، از طریق onInAppMessage).

مقدار بازگشتی: boolean به‌صورت همزمان: true وقتی ذخیره شد (روی گوشی می‌ماند). در وب و بدون AppMint Push همیشه false. نیازمندی: اندروید: 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. نیازمندی: اندروید: 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. نیازمندی: اندروید: AppMint Push روشن‌شده در مرحله یکپارچه‌سازی (مرحله 3) هنگام ساخت؛ هدف‌گیری تگ و خودکارسازی‌ها به اپلیکیشن‌های ساخته‌شده از 30 Sep 2026 به بعد می‌رسد.

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

// Abandoned cart: a tag automation on "cart" (say 60 minutes) reminds only phones whose cart is
// still open when the time is up. Setting 'open' again does not restart the clock.
export function useCartPushTag(itemCount: number) {
  const hasItems = itemCount > 0;
  useEffect(() => {
    setPushTag('cart', hasItems ? 'open' : '');
  }, [hasItems]);
}

export function onCheckoutComplete() {
  setPushTag('cart', '');
}

نتیجه را بررسی کنید: false یعنی برچسب رد شده است (کلید نامعتبر، مقدار بیش از ۱۲۸ نویسه، یا بیش از ۵۰ برچسب) یا این صفحه روی وب است؛ پس وضعیت را خودتان نگه دارید.

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. نیازمندی: اندروید: AppMint Push باید هنگام ساخت در مرحله Integrate (مرحله ۳) روشن شده باشد؛ هدف‌گیری برچسب و خودکارسازی‌ها به اپلیکیشن‌های ساخته‌شده از ۳۰ سپتامبر ۲۰۲۶ به بعد می‌رسد.

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

تگ‌ها را در یک effect ری‌اکت با حساب واردشده هماهنگ نگه دارید و آن‌هایی را که دیگر کاربرد ندارند حذف کنید.

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 می‌گوید کدام‌یک از کاربران شما روی این نصب وارد شده، تا مرکز ارسال بتواند پیامی به همان یک کاربر بفرستد. پس از ورود شناسهٔ حساب پایدار خودتان را بدهید و هنگام خروج ''.

مقدار بازگشتی: boolean به‌صورت همزمان - true وقتی شناسه ذخیره شد؛ false برای شناسه‌ای با هر چیز غیر از حروف، ارقام و _ @ . : + - (تا 128). در وب همیشه false. نیازمندی: اندروید: AppMint Push روشن‌شده در مرحله یکپارچه‌سازی (مرحله 3) هنگام ساخت؛ ارسال به یک کاربر به اپلیکیشن‌های ساخته‌شده از 30 Sep 2026 به بعد می‌رسد.

import { useEffect } from 'react';
import { setPushUser, getPushUser } from '@/lib/appmintNative';
import { supabase } from '@/lib/supabase';

// Keeps AppMint Push in step with the signed-in account: the auth user id, never the email.
export function usePushUser() {
  useEffect(() => {
    const { data } = supabase.auth.onAuthStateChange((_event, session) => {
      const id = session?.user.id ?? '';
      if (getPushUser() !== id) setPushUser(id);
    });
    return () => data.subscription.unsubscribe();
  }, []);
}

شناسه ردشده را مدیریت کنید: 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' });

نکته‌ها:

  • در اپلیکیشن اندروید خود پوسته نوار را رنگ می‌کند که روی اندروید ۱۵ لبه‌تا‌لبه هم کار می‌کند؛ رنگ غیرقابل تجزیه نادیده گرفته می‌شود - #RRGGBB بدهید. فراخوانی فقط با سبک (backgroundColor = null) رنگی را که این صفحه آخر گذاشته نگه می‌دارد و آیکون‌ها را برمی‌گرداند؛ تا وقتی صفحه رنگی نگذاشته آیکون‌ها را از طریق افزونه StatusBar برمی‌گرداند و نوار رنگ انتخاب‌شده هنگام ساخت را نگه می‌دارد.
  • در وب <meta name="theme-color"> را تنظیم می‌کند؛ مرورگر رنگ آیکون را خودش انتخاب می‌کند.

appmintNative.setVolume appmintNative#

setVolume(stream, percent, showUi?) — @/lib/appmintNative

نمونه

سطح صدای خود گوشی را برای یک جریان صوتی تنظیم می‌کند، از ۰ تا ۱۰۰. showUi نوار صدای گوشی را هنگام حرکت نشان می‌دهد.

مقدار برگشتی: { ok, stream?, current?, max?, percent?, 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.share appmintNative#

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

نمونه

شیت اشتراک‌گذاری سیستم را در اپلیکیشن، و Web Share API را در مرورگر باز می‌کند.

مقدار بازگشتی: Promise<boolean> - true وقتی شیت باز شد؛ false وقتی کاربر آن را بست یا اشتراک‌گذاری در دسترس نیست. وقتی خود اشتراک نامعتبر است (مثلاً یک url پشتیبانی‌نشده) یا اشتراک دیگری هنوز باز است رد می‌شود. نیازمندی: هیچ.

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

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

فقط متن (بدون پیوند) هم اشکالی ندارد:

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

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

شکل Capacitor Share.share(opts) همان کد را فراخوانی می‌کند اما وقتی کاربر لغو کند رد می‌شود (قرارداد Capacitor) و در غیر این صورت { activityType: '' } برمی‌گرداند؛ Share.canShare() برمی‌گرداند { value: can('share') !== 'none' }:

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

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

نکته‌ها:

  • فقط فیلدهایی را که دارید بدهید. در اپلیکیشن، متن و پیوندها از افزونهٔ Capacitor Share عبور می‌کنند.
  • files در اپلیکیشن Android کار می‌کند (فایل‌ها به‌عنوان پیوست واقعی به برگهٔ اشتراک‌گذاری می‌روند؛ یک url به متن اضافه می‌شود) و در وب جایی که مرورگر می‌تواند فایل به اشتراک بگذارد (navigator.canShare({ files })). فایل‌ها را کوچک نگه دارید - در حافظه به اپلیکیشن داده می‌شوند.
  • در وب آن را از یک ضربه صدا بزنید؛ مرورگرها اشتراکی را که عمل کاربر آغاز نکرده رد می‌کنند.

appmintNative.Share appmintNative#

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

نمونه

همراه با appmintNative.share مستند شده - نمونهٔ آنجا این یکی را هم نشان می‌دهد.

شیت اشتراک‌گذاری سیستم را در اپلیکیشن، و Web Share API را در مرورگر باز می‌کند.

مقدار بازگشتی: Promise<boolean> - true وقتی شیت باز شد؛ false وقتی کاربر آن را بست یا اشتراک‌گذاری در دسترس نیست. وقتی خود اشتراک نامعتبر است (مثلاً یک url پشتیبانی‌نشده) یا اشتراک دیگری هنوز باز است رد می‌شود. نیازمندی: هیچ.

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

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

فقط متن (بدون پیوند) هم اشکالی ندارد:

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

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

شکل Capacitor Share.share(opts) همان کد را فراخوانی می‌کند اما وقتی کاربر لغو کند رد می‌شود (قرارداد Capacitor) و در غیر این صورت { activityType: '' } برمی‌گرداند؛ Share.canShare() برمی‌گرداند { value: can('share') !== 'none' }:

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

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

نکته‌ها:

  • فقط فیلدهایی را که دارید بدهید. در اپلیکیشن، متن و پیوندها از افزونهٔ Capacitor Share عبور می‌کنند.
  • files در اپلیکیشن Android کار می‌کند (فایل‌ها به‌عنوان پیوست واقعی به برگهٔ اشتراک‌گذاری می‌روند؛ یک url به متن اضافه می‌شود) و در وب جایی که مرورگر می‌تواند فایل به اشتراک بگذارد (navigator.canShare({ files })). فایل‌ها را کوچک نگه دارید - در حافظه به اپلیکیشن داده می‌شوند.
  • در وب آن را از یک ضربه صدا بزنید؛ مرورگرها اشتراکی را که عمل کاربر آغاز نکرده رد می‌کنند.

appmintNative.sharedText appmintNative#

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

نمونه

متن یا پیوندی را که اپلیکیشن دیگری به این اپلیکیشن به اشتراک گذاشته می‌دهد («اشتراک‌گذاری ← اپلیکیشن شما»)، مثلاً پیوندی که از مرورگر یا YouTube به اشتراک گذاشته شده است.

مقدار برگشتی: به‌صورت همزمان، { text, subject, url, receivedAt } یا null وقتی چیزی منتظر نیست. url اولین پیوند http(s) پیداشده در text است ('' اگر نباشد)؛ receivedAt میلی‌ثانیه از سال ۱۹۷۰ است. نیازمندی: گزینه Receive Shares From Other Apps ساخت با «Text and links» - وقتی کد شما sharedText( یا فراخوانی می‌کند خودکار روشن می‌شود onShared(.

هنگام mount بررسی کنید، ذخیره کنید، سپس پاک کنید:

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

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

به اشتراکی که وقتی اپلیکیشن از قبل باز است می‌رسد با onShared گوش دهید و پس از استفاده آن را پاک کنید.

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

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

یک فرم را از پیوند اشتراکی پر کنید و در وب چیزی نشان ندهید، جایی که sharedText() است null.

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

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

نکته‌ها:

  • اشتراک تا وقتی clearShared() را فراخوانی کنید نگه داشته می‌شود، پس اپلیکیشنی که بعد از اشتراک بالا می‌آید هنوز آن را می‌بیند. پس از ذخیره آن را پاک کنید، وگرنه بالا آمدن بعدی دوباره ذخیره‌اش می‌کند.
  • برای اشتراکی که وقتی اپلیکیشن باز است می‌رسد، از این استفاده کنید onShared().
  • عکس یا ویدیویی که همراه با شرح به اشتراک گذاشته شده: شرح اینجا می‌رسد، فایل از طریق API فایل بازشده.
  • فقط اپلیکیشن Android. can('shareTarget') در وب 'none' است، و sharedText() آنجا null برمی‌گرداند.

appmintNative.sheet appmintNative#

sheet(opts: { title?, content, dismissible? }): { close(), element, closed }

نمونه

یک شیت پایین مودال با رفتار واقعی شیت باز می‌کند - برای پنل فیلتر، نوشتن پیام یا پیش‌نمایش جزئیات.

مقدار برگشتی: یک handle: 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 که با یک portal داخل شیت رندر می‌شود:

import { useState } from 'react';
import { createPortal } from 'react-dom';
import { sheet } from '@/lib/appmintNative';

export function FiltersButton() {
  const [host, setHost] = useState<HTMLElement | null>(null);
  const [close, setClose] = useState<() => void>(() => () => {});

  const open = () => {
    const h = sheet({ title: 'Filters', content: () => {} });
    setHost(h.element);
    setClose(() => h.close);
    void h.closed.then(() => setHost(null));      // unmount the portal after any close
  };

  return (
    <>
      <button onClick={open}>Filters</button>
      {host && createPortal(
        <form onSubmit={(e) => { e.preventDefault(); close(); }}>
          <label><input type="checkbox" name="open" /> Open now</label>
          <button type="submit">Apply</button>
        </form>,
        host,
      )}
    </>
  );
}

برگه‌ای که تا انتخاب کاربر باز می‌ماند، با استفاده از 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()

نمونه

میان‌برهایی به منوی لمس طولانی آیکون اپلیکیشن اضافه می‌کند (میان‌برهای پویای اندروید). یک ضربه اپلیکیشن را در آن مسیر باز می‌کند.

مقدار برگشتی: 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]);
}

نکته‌ها:

  • حداکثر ۴ مورد؛ موارد اضافه حذف می‌شوند. به route بدون / ابتدایی یکی اضافه می‌شود. id اختیاری است.
  • Android 7.1 و بالاتر؛ در Android قدیمی‌تر فراخوانی کاری نمی‌کند. عنوان‌ها به 25 نویسه (برچسب کوتاه) و 50 نویسه (برچسب بلند) بریده می‌شوند.
  • یک ضربه اپلیکیشن را با پیوند راه‌اندازی appmint-shortcut://<package><route> باز می‌کند، پس new URL(u).pathname مسیر شماست. آن را هنگام mount با launchUrl() و تا وقتی اپلیکیشن اجرا می‌شود با onDeepLink() بخوانید.
  • can('shortcuts') در وب 'none' است: مرورگر آیکون اپلیکیشن ندارد.

appmintNative.snoozeAlarm appmintNative#

snoozeAlarm(id?, minutes?) — @/lib/appmintNative

نمونه

زنگ هشدار را قطع می‌کند و بعد از minutes دوباره به صدا درمی‌آورد (۰ یا حذف‌شده = snoozeMinutes خود هشدار، پیش‌فرض ۱۰). زنگ فردای هشدار تکرارشونده جابه‌جا نمی‌شود.

مقدار بازگشتی: 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 } (پیش‌فرض‌ها: زبان موتور، ۱، ۱). نیازمندی: هیچ.

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 یک utterance مستقیم استفاده کنید؛ در اپلیکیشن Android هم کار می‌کند.

appmintNative.speech appmintNative#

speech — @/lib/appmintNative

نمونه

دیکته: گفتار کاربر را به متن تبدیل می‌کند. تشخیص گفتار اندروید در اپلیکیشن اندروید، 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 }. نیازمندی: میکروفون در مرحله ۴ (دسترسی)؛ ساخت‌های هوش مصنوعی وقتی کد از تشخیص گفتار استفاده کند آن را برای شما روشن می‌کنند.

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

نکته‌ها:

  • در اپلیکیشن اندروید 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' یعنی پس‌زمینه تیره (آیکون‌های روشن)، هر چیز دیگر یعنی آیکون‌های تیره. در اپلیکیشن اندروید پل پوسته نوار را رنگ می‌کند (فراخوانی رنگ افزونه در اندروید 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)} />;
}

نکته‌ها: وقتی چیزی در حال خواندن نیست بی‌خطر است. در اپلیکیشن اندروید: توقف موتور بومی. در وب: 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();

یک کارت را با کشیدن به بالا رد کنید، با مدیری که هنگام unmount برداشته می‌شود:

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 گوشی را می‌خواند (در اندروید 12 به بالا از تصویر زمینه ساخته می‌شود)، تا تأکید اپلیکیشن مثل اپلیکیشن‌های سیستم تصویر زمینه کاربر را دنبال کند.

مقدار برگشتی: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - هر نمونه رنگ یک تُن ('0'، '10'، '50'، '100' … '900'، '1000') را به یک رنگ هگز نگاشت می‌کند. { available: false } زیر اندروید ۱۲ و در وب. نیازمندی: هیچ.

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(…) سیگنال پایان است: مرحله بعد از گفتار می‌تواند مستقیماً بعد از آن بیاید. در اپلیکیشن: موتور بومی تبدیل متن به گفتار اندروید در پوسته؛ در وب: speechSynthesis.

appmintNative.textZoom appmintNative#

textZoom.get() / textZoom.getPreferred() / textZoom.set(value)

نمونه

کنترل اندازه متن درون‌برنامه. در اپلیکیشن، بزرگ‌نمایی متن صفحه را تغییر می‌دهد (1 = ۱۰۰٪)؛ در وب، اندازه فونت ریشه را تغییر می‌دهد که همه طرح‌بندی‌های مبتنی بر 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.' });

نکته‌ها:

  • در اپلیکیشن اندروید افزونه Toast در Capacitor آن را در پایین صفحه نشان می‌دهد.
  • در همه جای دیگر یک snackbar کوچک است که روی صفحه با رنگ‌های خود اپلیکیشن (--ink روی --bg)، بالای حاشیهٔ ناحیهٔ امن، با role="status" رسم می‌شود تا صفحه‌خوان‌ها آن را اعلام کنند.
  • هر بار فقط یکی نشان داده می‌شود؛ توست جدید جای قبلی را می‌گیرد.
  • toast عمداً به‌راحتی از نظر دور می‌ماند. هرگز آن را برای خطایی که کاربر باید رویش اقدام کند استفاده نکنید.

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

نکته‌ها:

  • در اپلیکیشن اندروید افزونه Toast در Capacitor آن را در پایین صفحه نشان می‌دهد.
  • در همه جای دیگر یک snackbar کوچک است که روی صفحه با رنگ‌های خود اپلیکیشن (--ink روی --bg)، بالای حاشیهٔ ناحیهٔ امن، با role="status" رسم می‌شود تا صفحه‌خوان‌ها آن را اعلام کنند.
  • هر بار فقط یکی نشان داده می‌شود؛ توست جدید جای قبلی را می‌گیرد.
  • toast عمداً به‌راحتی از نظر دور می‌ماند. هرگز آن را برای خطایی که کاربر باید رویش اقدام کند استفاده نکنید.

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() ← مقدار منطقی. نیازمندی: هیچ، بدون مجوز.

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 آن track روشنش کنید.

appmintNative.trackEvent appmintNative#

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

نمونه

یک لحظه در اپلیکیشن شما را گزارش می‌کند تا پیام‌های درون‌برنامه‌ای سازنده با محرک «وقتی رویدادی رخ می‌دهد» (همان نام) نمایش داده شوند. اپلیکیشن کمپین‌های سازنده را از مرکز ارسال AppMint Push دانلود می‌کند و خودش نشان می‌دهد.

مقدار برگشتی: boolean به‌صورت همزمان: false برای نام نامعتبر (حرف، رقم و _ . : -، تا ۶۴)، در وب، و در اپلیکیشن‌های ساخته‌شده بدون AppMint Push. نیازمندی: اندروید: AppMint Push باید هنگام ساخت در مرحله Integrate (مرحله ۳) روشن شده باشد.

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

یک hook که اولین باز شدن یک صفحه را دنبال می‌کند:

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 تغییر state شماست (یک 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>
  );
}

نکته‌ها:

  • اگر WebView از View Transitions API پشتیبانی کند از آن استفاده می‌کند. در غیر این صورت به‌روزرسانی را اعمال می‌کند و یک انیمیشن ورود کوتاه روی #root (یا اولین فرزند <body>).
  • وقتی کاربر از سیستم حرکت کاهش‌یافته خواسته، فقط به‌روزرسانی را اعمال می‌کند - بدون انیمیشن.
  • انیمیشن‌ها حدود 240 ms طول می‌کشند.

appmintNative.unlock appmintNative#

unlock(title?, subtitle?) — from '@/lib/appmintNative'

نمونه

از صاحب گوشی می‌خواهد حضورش را ثابت کند - اثر انگشت، چهره، یا PIN/رمز دستگاه - و به شما می‌گوید که انجام داد یا نه.

مقدار برگشتی: Promise<boolean> - true وقتی باز شد؛ false وقتی لغو شد، شکست خورد، قفل شد، یا چیزی روی این دستگاه نمی‌تواند بررسی کند. هرگز خطا نمی‌اندازد. نیازمندی: در اپلیکیشن، اثر انگشت در مرحله ۴ (دسترسی) هنگام ساخت. در وب: احراز‌کنندهٔ پلتفرم مرورگر (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'

حذف را با یک باز کردن قفل تازه محافظت کنید، سپس با لرزش و toast تأیید کنید:

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

export async function deleteAccountData(wipe: () => Promise<void>) {
  const ok = await unlock('Confirm delete', 'This removes your saved data');
  if (!ok) { haptic('warning'); toast('Not unlocked, nothing was deleted'); return; }
  await wipe();
  haptic('success');
  toast('Data deleted');
}

نکته‌ها: در اپلیکیشن، PIN، الگو یا رمز دستگاه به‌عنوان راه دوم ورود مجاز است. در وب، اولین فراخوانی احراز‌کننده پلتفرم مرورگر را ثبت می‌کند و بازشده حساب می‌شود. این ثابت می‌کند صاحب دستگاه حاضر است؛ ورود نیست - هرگز آن را تنها محافظ داده‌ای که روی سرور است نکنید. در اندروید ۱۱ و جدیدتر پرسش روی هر گوشی با قفل صفحه باز می‌شود - گوشی فقط با PIN هم آن را می‌گیرد و با PIN باز می‌کند؛ در اندروید ۱۰ و قدیمی‌تر باید اول یک اثر انگشت ذخیره شده باشد، وگرنه بدون پرسش false پاسخ می‌دهد.

appmintNative.updates appmintNative#

updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()

نمونه

یک به‌روزرسان سمت صفحه برای به‌روزرسانی بسته‌های وب. کد صفحه به آن نیاز ندارد: در اپلیکیشن AppMint، به‌روزرسانی از راه دور را اپلیکیشن اندروید به‌صورت بومی انجام می‌دهد و 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>;
}

نکته‌ها:

  • در اپلیکیشن اندروید، مدیر به‌روزرسانی از راه دور پوسته به‌روزرسانی‌ها را دانلود، تأیید و ارائه می‌کند و بسته‌ای را که بارگذاری نمی‌شود برمی‌گرداند. صفحه را خود پوسته ارائه می‌دهد، پس بسته‌ای که از کد صفحه آماده شود هرگز نمایش داده نمی‌شود - به همین دلیل supported() است false.
  • در وب چیزی برای به‌روزرسانی نیست؛ صفحه همان است که سرور می‌فرستد.
  • به‌روزرسانی‌ها فقط می‌توانند HTML، CSS و JavaScript را تغییر دهند؛ یک قابلیت بومی جدید همچنان به ساخت جدید اپلیکیشن نیاز دارد.

appmintNative.watchIdle appmintNative#

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

نمونه

به اپلیکیشن می‌گوید کاربر کی بیکار می‌شود یا صفحه را قفل می‌کند، و کی برمی‌گردد (can('idle')). بار اول آن را با یک ضربه شروع کنید - درخواست مجوز به یکی نیاز دارد.

مقدار برگشتی: Promise تابع توقف. callback در شروع و در هر تغییر { 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>
  );
}

حالت دور بودن را نشان دهید و هنگام unmount نظارت را متوقف کنید؛ مجوز ردشده با آن گزارش می‌شود 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 };
}

وقتی کاربر دور است یک تایمر polling را مکث کنید و وقتی برگشت تازه‌سازی کنید:

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؛ شش تا همیشه نشان داده می‌شود، بیشتر وقتی ویجت به‌اندازه کافی بلند باشد؛ هر خط یک رشته یا { 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() برای textSize/align ناشناخته یا رنگ خط نامعتبر false برمی‌گرداند.
  • یک ضربه اپلیکیشن را با appmint-shortcut://<package><route> باز می‌کند؛ آن را بخوانید با launchUrl() / onDeepLink().
  • requestPin() به Android 8 یا بالاتر و لانچری که از آن پشتیبانی کند نیاز دارد. clear() کارت خالی «هنوز چیزی برای نمایش نیست» را نشان می‌دهد.
  • فقط اپلیکیشن اندروید: can('widget') در وب 'none' است، جایی که set() و requestPin() برمی‌گردانند false.