أنشئ APK مجانًا

واجهة JavaScript bridge API

appmintNative (تطبيقات مبنية بالذكاء الاصطناعي)

الأغلفة المصنّفة التي يستوردها كل مشروع مبني بالذكاء الاصطناعي من @/lib/appmintNative. يختار كل منها المسار الأصلي داخل التطبيق ومسار ويب، أو none بصدق، في المتصفح - استدعاء واحد يعمل في كل مكان.

appmintNative.actionSheet appmintNative#

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

مثال

يعرض ورقة سفلية "اختر واحدًا من هذه" - عنصر التحكم الأصلي لقائمة إجراءات الصف.

القيمة المُرجعة: Promise<number | null> - فهرس الخيار المختار، أو null عندما أغلق المستخدم الورقة (نقر على الخلفية، أو Escape، أو إلغاء النظام). الإغلاق أمر طبيعي وليس خطأ. المتطلبات: لا شيء.

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

export async function onNoteMenu(note: { id: string }, api: {
  edit(id: string): void; share(id: string): void; remove(id: string): void;
}) {
  const i = await actionSheet('Note', [
    { title: 'Edit' },
    { title: 'Share' },
    { title: 'Delete', destructive: true },
  ]);
  if (i === null) return;                 // dismissed
  if (i === 0) api.edit(note.id);
  if (i === 1) api.share(note.id);
  if (i === 2) { haptic('warning'); api.remove(note.id); }
}

افتحها من زر ظاهر، ومن ضغطة مطولة على الصف:

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

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

الشكل بأسلوب Capacitor ActionSheet.showActions({ title?, options }) يأخذ الخيارات نفسها ويُحلّ { index }، مع -1 عند الإغلاق دون اختيار:

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

const { index } = await ActionSheet.showActions({
  title: 'Photo',
  options: [{ title: 'Save' }, { title: 'Delete', destructive: true }],
});
if (index === 1) console.log('delete');   // -1 = dismissed; never test with `if (index)`

ملاحظات:

  • في تطبيق Android ترسم إضافة ActionSheet الخاصة بـ Capacitor ورقة النظام؛ ويستخدم 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 ترسم إضافة ActionSheet الخاصة بـ Capacitor ورقة النظام؛ ويستخدم destructive نمطها المدمّر.
  • في غير ذلك هي ورقة حقيقية بألوان التطبيق (--surface و--ink و--danger)، مع خلفية معتمة وEscape ونقل التركيز إلى الورقة واهتزاز اختيار.
  • قائمة options فارغة تُحلّ بـ null فورًا. لأي محتوى آخر غير قائمة إجراءات، استخدم sheet().

appmintNative.addContact appmintNative#

addContact(contact: Contact): Promise<boolean>

مثال

يحفظ جهة اتصال جديدة في دفتر عناوين الهاتف. تطبيق Android فقط: يحفظها جسر الغلاف مباشرة (دون فتح شاشة جهات الاتصال).

القيم المُعادة: Promise<boolean> - true بعد الحفظ، وfalse عندما رفض المستخدم إذن جهات الاتصال أو فشل الحفظ. يُرفض مع Error قيمة code فيه 'not_enabled' (لم يفعّل الإنشاء كتابة جهات الاتصال) أو 'foreign_origin'، وبعد انتظار 5 دقائق دون إجابة. المتطلبات: مفتاح تعديل جهات الاتصال في الخطوة 4 (الوصول) - يشغّله الإنشاء عندما يستدعي كودك هذا - وإذن المستخدم عند أول استخدام.

احفظ عميلًا من نموذج.

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

export async function saveCustomer(name: string, phone: string, email: string): Promise<string> {
  if (can('contactAdd') === 'none') return 'Saving contacts works in the app only.';
  try {
    const ok = await addContact({ name, phones: [phone], emails: email ? [email] : [] });
    return ok ? 'Saved to contacts' : 'Allow contacts access to save it';
  } catch (e) {
    return (e as { code?: string }).code === 'not_enabled'
      ? 'Saving contacts is not switched on in this app.'
      : 'No answer from the phone. Try again.';     // 5-minute timeout
  }
}

تخطَّ جهة اتصال محفوظة بالفعل. يقرأ searchContacts دفتر العناوين، لذا تحقق قبل الإضافة:

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

export async function addIfNew(name: string, phone: string): Promise<'added' | 'exists' | 'refused' | 'app_only'> {
  if (can('contactAdd') === 'none') return 'app_only';
  const found = await searchContacts(name, 5);
  if (found.some((c) => c.phones?.includes(phone))) return 'exists';
  return (await addContact({ name, phones: [phone] })) ? 'added' : 'refused';
}

ميّز بين خطأ في الإنشاء ورفض. الرفض الذي يحمل code 'foreign_origin' أو 'not_enabled' هو مشكلة في الإنشاء، أما false فهو اختيار المستخدم:

import { useState } from 'react';
import { addContact } from '@/lib/appmintNative';

export function SaveContactButton({ name, phone }: { name: string; phone: string }) {
  const [msg, setMsg] = useState('');
  async function onTap() {
    try {
      setMsg((await addContact({ name, phones: [phone] })) ? 'Saved.' : 'Contacts access was declined.');
    } catch (e) {
      const code = (e as { code?: string }).code;
      setMsg(code === 'foreign_origin' || code === 'not_enabled' ? 'This build cannot save contacts.' : 'Try again.');
    }
  }
  return <button onClick={onTap}>{msg || 'Save to contacts'}</button>;
}

ملاحظات: يحفظ فورًا - لا يُفتح محرر جهة اتصال - فأكّد مع المستخدم في شاشتك أولًا. يحفظ أرقام الهاتف كأرقام جوال وجهة الاتصال على الجهاز (بلا حساب). كتابة جهات الاتصال حساسة: صرّح بها في نموذج أمان البيانات في Play وسياسة الخصوصية.

appmintNative.alarmSupport appmintNative#

alarmSupport() — @/lib/appmintNative

مثال

ما إذا كان scheduleAlarm يستطيع ضبط منبّه الآن، وما الناقص إن لم يستطع.

القيم المُعادة: 'ready' أو 'not_enabled' (أُنشئ بدون منبّه الساعة) أو 'needs_exact_alarm' (Android 12+: المنبّهات الدقيقة متوقفة) أو 'needs_notification_permission' (Android 13+) أو 'unavailable' (الويب وiOS - لا ترسم عناصر التحكم بالمنبّه هناك). المتطلبات: 🔔 منبّه الساعة في خطوة الوصول (الخطوة 4) عند الإنشاء.

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

export function AlarmSetupBanner() {
  const s = alarmSupport();
  if (s === 'ready' || s === 'unavailable') return null;
  const text = {
    not_enabled: 'This app was built without alarm clocks.',
    needs_exact_alarm: 'Allow "Alarms & reminders" for this app in Settings.',
    needs_notification_permission: 'Allow notifications so you can stop the alarm.',
  }[s];
  return <p role="alert">{text}</p>;
}

اضبط منبّهًا يوميًا فقط عندما يكون الهاتف جاهزًا، وقل السبب عندما لا يكون:

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

export function setMorningAlarm(hour: number, minute: number): string {
  const s = alarmSupport();
  if (s !== 'ready') return `Alarm not set (${s}).`;       // 'not_enabled', 'needs_exact_alarm', ...
  const first = new Date();
  first.setHours(hour, minute, 0, 0);
  if (first.getTime() <= Date.now()) first.setDate(first.getDate() + 1);
  const ok = scheduleAlarm('morning', { at: first.getTime(), title: 'Wake up', repeat: 'daily', snoozeMinutes: 5 });
  return ok ? 'Alarm set.' : 'The phone did not accept the alarm.';
}

أخفِ شاشة المنبّه على الويب وتفاعل عند انتهاء منبّه، بالدمج مع onAlarm:

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

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

appmintNative.App appmintNative#

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

مثال

اسم التطبيق ومعرّف حزمته وإصداره، وطريقة لإغلاق التطبيق، وأحداثه - بشكل مكوّن App في Capacitor.

القيمة المُعادة: getInfo() → Promise<{ name: string; id: string; build: string; version: string }>. exitApp() → void. addListener(event, fn) → شكل Capacitor (Promise للمقبض الذي يملك أيضًا remove()). الأحداث: appStateChange ({ isActive })، pause، resume (ذهاب الصفحة إلى الخلفية / عودتها منها)، appUrlOpen ({ url }، رابط عميق - مثل 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 في الغلاف داخل التطبيق ولا يفعل شيئًا على الويب، لذا لا تجعل الخروج الطريق الوحيد للمتابعة. لأحداث الإيقاف المؤقت/الاستئناف والروابط العميقة انظر Capacitor.Plugins.App وlaunchUrl() و onDeepLink().

appmintNative.appLauncher appmintNative#

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

مثال

يفتح تطبيقًا آخر برابط - whatsapp://send?text=… أو tel: أو mailto: أو geo: أو رابط https يسلّمه النظام إلى تطبيقه - ويسأل أولًا ما إذا كان ذلك ممكنًا.

القيم المُعادة: canOpen() ← Promise<boolean>. open() ← Promise<boolean> (false عندما لم يأخذ أي تطبيق الرابط). المتطلبات: لا شيء.

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

async function chat(phone: string, text: string) {
  const url = `whatsapp://send?phone=${phone}&text=${encodeURIComponent(text)}`;
  if (!(await appLauncher.canOpen(url))) {
    showMessage('WhatsApp is not installed on this phone.');
    return;
  }
  await appLauncher.open(url);
}

اتصال أو بريد إلكتروني (لا حاجة للتحقق؛ فالهاتف دائمًا لديه تطبيق لهذا):

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

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

افتح تطبيق الخرائط برابط geo:، وأخبر المستخدم عندما لا يأخذه أي تطبيق:

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

export async function showOnMap(lat: number, lng: number, label: string) {
  const ok = await appLauncher.open(`geo:${lat},${lng}?q=${lat},${lng}(${encodeURIComponent(label)})`);
  if (!ok) showMessage('No maps app could open this place.');
}

ملاحظات: داخل التطبيق يستخدم مكوّن Capacitor AppLauncher. على Android 11+ لا يرى canOpen سوى المخططات التي يصرّح بها التطبيق (https وgeo وgoogle.navigation وtel وmailto وsms وwhatsapp وintent وupi)؛ وللمخططات الأخرى يُجيب false حتى لو كان التطبيق مثبّتًا - استدعِ open() وافحص نتيجته بدلًا من ذلك. على الويب لا يمكن فحص مخطط مخصص، لذا تكون canOpen قيمة true فقط لروابط http(s)، ويقوم open بالانتقال إلى الرابط.

appmintNative.appLock appmintNative#

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

مثال

يقفل التطبيق مجددًا عندما يعود المستخدم بعد أن بقي في الخلفية لبعض الوقت: يطلب بصمة / وجهًا / PIN (unlock())، ويفعّل شاشة الخصوصية حتى لا يعرض مبدّل التطبيقات محتواك.

القيمة المُرجعة: دالة توقف القفل (استدعِها عند تسجيل الخروج أو إزالة المكوّن). onLocked(true) تُطلق عند قفل التطبيق، وonLocked(false) بعد فتح ناجح. المتطلبات: مثل unlock() - أندرويد: تحديد بصمة الإصبع عند الإنشاء.

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

/** Call once in your root component; render a lock screen while it returns true. */
export function useAppLock(): boolean {
  const [locked, setLocked] = useState(false);
  useEffect(() => appLock({
    timeoutSec: 60,                 // lock after 1 minute in the background (default 30)
    title: 'Unlock MyNotes',
    onLocked: setLocked,            // true → cover the screen, false → show the app again
  }), []);                          // appLock returns its own stop function = the cleanup
  return locked;
}

في المكوّن الجذري: const locked = useAppLock(); if (locked) return <LockScreen />;.

أوقف القفل عند تسجيل الخروج، حتى لا يُطلب من مستخدم سجّل خروجه فتح القفل:

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

let stopLock: (() => void) | null = null;

export function onSignedIn() {
  stopLock = appLock({ timeoutSec: 120, title: 'Unlock your wallet' });
}

export function onSignedOut() {
  stopLock?.();            // removes the listener and turns the privacy screen off
  stopLock = null;
}

اعرض القفل فقط حيث يستطيع الجهاز فتحه، واقرنه مع unlock() لزر "اقفل الآن" اليدوي:

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

export function LockSettings({ onLockedChange }: { onLockedChange: (locked: boolean) => void }) {
  if (can('unlock') === 'none') return <p>This device has no screen lock to use.</p>;   // appLock would do nothing here
  return (
    <>
      <button onClick={() => appLock({ onLocked: onLockedChange })}>Turn on app lock</button>
      <button onClick={async () => { onLockedChange(true); if (await unlock('Unlock')) onLockedChange(false); }}>
        Lock now
      </button>
    </>
  );
}

ملاحظات: إذا لم يكن لدى الجهاز ما يُفتح به القفل (can('unlock') === 'none') فلا تفعل الدالة شيئًا وتُرجع دالة فارغة - فقفل تطبيق لا يمكن فتحه أبدًا سيحبس المستخدم. إذا ألغى المستخدم، تسأل مجددًا حتى خمس مرات، ثم يبقى مقفلًا (أبقِ شاشة القفل ظاهرة) حتى ينتقل التطبيق إلى الخلفية ويعود. وهذا منفصل عن خيار قفل PIN في معالج الإنشاء، الذي يعرضه التطبيق أصليًا عند التشغيل.

appmintNative.appUpdate appmintNative#

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

مثال

يتحقق في Google Play من وجود إصدار أحدث ويبدأ تحديث Play داخل التطبيق (ينزّل ويثبّت في مكانه).

القيم المُعادة: check() ← Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null> (null على الويب، أو بدون خدمات Play، أو عند فشل Play). updateAvailable هو جواب المتجر نفسه. start() ← Promise<boolean>: true فقط عند اكتمال تحديث Play، وfalse عندما ألغى المستخدم أو فشل أو لم يتوفر تحديث. المتطلبات: لا شيء؛ يجب تثبيت التطبيق من Google Play.

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

async function offerUpdate() {
  const info = await appUpdate.check();
  if (!info?.inPlace) return;                    // Play can install an update now
  showBanner('A new version is ready', async () => {
    const done = await appUpdate.start();        // Play's full-screen flow / the App Store page
    if (!done) showBanner('Update later from the store.', null);
  });
}

قارن رمز إصدار Play بالمثبّت، وعامل null على أنه "لا يمكن التحقق هنا":

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

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

لافتة React تفحص مرة واحدة عند فتح الشاشة:

import { useEffect, useState } from 'react';
import { appUpdate, type AppUpdateInfo } from '@/lib/appmintNative';

export function UpdateBanner() {
  const [info, setInfo] = useState<AppUpdateInfo | null>(null);
  useEffect(() => { void appUpdate.check().then(setInfo); }, []);
  if (!info?.updateAvailable) return null;
  return <button onClick={() => void appUpdate.start()}>Update now</button>;
}

ملاحظات: updateAvailable هو جواب Play نفسه؛ ويضيف inPlace أن Play تستطيع تشغيل تحديثها داخل التطبيق الآن. availableVersion هو رمز إصدار Play (لا تعطي Play اسمًا) - قارنه بـ currentVersionCode، وليس بـ currentVersion الذي هو اسم الإصدار.

appmintNative.askForReview appmintNative#

askForReview(options): Promise<ReviewOutcome>

مثال

يطلب من Google Play عرض ورقة التقييم الخاصة به داخل التطبيق ويخبرك بما حدث.

القيمة المُعادة: Promise<ReviewOutcome> - { result, openedStore, message? } مع result 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' ('unavailable' على الويب). يفتح { openStore: true } صفحة Play عندما لم تظهر الورقة. المتطلبات: لا شيء؛ تظهر الورقة فقط لتطبيق مثبّت من Google Play.

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

export async function onRateTap() {
  const r = await askForReview({ openStore: true });   // listing if Play shows no sheet
  if (r.result === 'shown' || r.openedStore) toast('Thanks for rating!');
}

أخبر المستخدم بما حدث لكل نتيجة:

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

export async function rateMessage(): Promise<string> {
  const r = await askForReview();
  switch (r.result) {
    case 'shown': return 'Thanks for rating!';
    case 'not_from_play': return 'Rating works for installs from Google Play.';
    case 'quota': return 'Google limits how often the sheet shows. Try again later.';
    case 'unavailable': return '';                       // the web: draw no rating button at all
    default: return `Could not open the rating sheet${r.message ? `: ${r.message}` : ''}.`;
  }
}

اسأل مرة واحدة، بعد أن ينهي المستخدم مهمته الثالثة، وتذكّر أنك سألت:

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

export async function maybeAskAfterTask(completed: number) {
  if (completed !== 3 || localStorage.getItem('asked-review')) return;
  localStorage.setItem('asked-review', '1');
  await askForReview();
}

ملاحظات: اطلب بعد لحظة نجاح وفي حالات نادرة. التثبيت التجريبي من خارج المتجر يُجيب دائمًا not_from_play.

appmintNative.backgroundLocation appmintNative#

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

مثال

موقع يستمر في الورود بينما التطبيق في الخلفية: start, stop, onFix, buffered, clearBuffered, requestAlways, permissionState, available.

القيم المُعادة: start(options) ← Promise<BgResult> ({ ok, background, backgroundPermission, precise } أو { ok: false, reason }). stop / clearBuffered ← Promise<boolean>. buffered() ← Promise<LocationFix[]>. requestAlways() ← Promise<{ foreground, background, precise, error? }>. onFix(cb) ← دالة إلغاء اشتراك؛ ويحصل cb على تفاصيل appmint:location في كل مرة. المتطلبات: الموقع في الخلفية في الإنشاء؛ وإذن الموقع (اطلبه أولًا بـ navigator.geolocation).

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

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

اطلب "السماح طوال الوقت" قبل البدء، واشرح الرفض:

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

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

React hook يعرض النقاط الحية ويتوقف عن الاستماع عند إغلاق الشاشة:

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

export function useLastFix(): LocationFix | null {
  const [fix, setFix] = useState<LocationFix | null>(null);
  useEffect(() => backgroundLocation.onFix((d) => {
    if (d.type === 'fix') setFix(d);          // { type: 'error', reason } events are skipped here
  }), []);                                    // onFix returns its unsubscribe
  return fix;
}

appmintNative.backgroundSync appmintNative#

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

مثال

طلبات HTTP أصلية تعمل أثناء إغلاق التطبيق: enqueue (حفظ تلقائي دائم لمرة واحدة يُرسل عند الاتصال)، schedule (طلب متكرر، كل 15 دقيقة على الأقل)، cancel و list.

القيمة المُعادة: enqueue / schedule → Promise<boolean>؛ cancel → Promise<void>؛ list → Promise<BackgroundJob[]> ({ id, url, periodic, createdAt?, pendingDelivery? } - pendingDelivery على Android: مهمة لمرة واحدة لم تُسلَّم بعد). المتطلبات: لا شيء في تطبيق Android (يستخدم WorkManager). على الويب تكون can('backgroundSync') هي 'none' وتنتهي enqueue / schedule false.

حفظ تلقائي دائم: المعرّف نفسه يستبدل الحفظ الذي لم يُرسل بعد.

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

export async function saveDraft(draft: object, token: string) {
  if (can('backgroundSync') === 'none') return false;   // web: save normally instead
  return backgroundSync.enqueue({
    id: 'draft',
    url: 'https://api.example.com/drafts',
    method: 'PUT',
    headers: { Authorization: `Bearer ${token}` },
    body: JSON.stringify(draft),
  });
}

مزامنة متكررة، عرض وإلغاء:

await backgroundSync.schedule({
  id: 'sync', url: 'https://api.example.com/sync', body: '{}', intervalMinutes: 60,
});

const jobs = await backgroundSync.list();
console.log(jobs.map((j) => `${j.id} ${j.periodic ? 'repeating' : 'one-off'}`));

await backgroundSync.cancel('sync');

الحدث الخام (ما ينتظره الغلاف على Android):

window.addEventListener('appmint:work', function (e) {
  // { id, status: 'enqueued' | 'scheduled' | 'cancelled' | 'failed', reason?, intervalMinutes? }
  // or { requestId, jobs: [...] } for a list
  console.log('work', e.detail);
});

ملاحظات: تعني true أن المهمة وُضعت في الطابور لا أنها سُلّمت. وتعني false أنها لم توضع في الطابور: أجاب الغلاف status: 'failed' (رابط لا يبدأ بـ http، أو معرّف فارغ، أو رفض النظام) أو لم يجب خلال 10 ثوانٍ. لا يعمل JavaScript الخاص بالصفحة في الخلفية أبدًا؛ هذا الطلب فقط. لا يُعاد الرد 4xx؛ وتُعاد أخطاء الشبكة و5xx حتى 8 مرات.

appmintNative.backgroundTasks appmintNative#

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

مثال

كود الصفحة نفسه، يعمل بشكل دوري والتطبيق مغلق (غلاف مكتوب فوق AppMint.background): register, unregister, list, runNow, done, isBackgroundRun, currentRun, onRun, available.

القيم المُعادة: register / runNow ← Promise<BgResult>؛ unregister / done ← Promise<boolean>؛ list() ← المهام المسجّلة مع lastRunAt / lastResult؛ isBackgroundRun() / currentRun() متزامنان. المتطلبات: تشغيل كود الصفحة أثناء الإغلاق في الإنشاء.

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

// In the app's entry file, before rendering:
if (backgroundTasks.isBackgroundRun()) {
  backgroundTasks.onRun(async (run) => {
    try {
      const unread = await fetch('/api/unread').then((r) => r.json());
      if (unread.count > 0) await notify({ title: 'New messages', body: `${unread.count} unread` });
      await backgroundTasks.done(run.runId, { ok: true });
    } catch {
      await backgroundTasks.done(run.runId, { ok: false, retry: true });
    }
  });
} else {
  void backgroundTasks.register('unread', { intervalMinutes: 30, requiresNetwork: true });
  // renderApp();
}

سجّل عند تسجيل الدخول، وأزل عند تسجيل الخروج، واسرد ما هو مسجَّل:

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

export async function onSignedIn() {
  const r = await backgroundTasks.register('sync', { intervalMinutes: 60, requiresNetwork: true });
  if (!r.ok) console.warn('Background sync not set up:', r.reason);   // 'not_enabled' or 'unsupported'
}

export async function onSignedOut() {
  await backgroundTasks.unregister('sync');
}

export async function taskCountText(): Promise<string> {
  const tasks = await backgroundTasks.list();
  return tasks.length ? `Registered tasks: ${tasks.length}` : 'No background tasks.';
}

شغّل مهمة الآن من زر "زامن الآن" وافحص الإجابة:

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

export function SyncNowButton() {
  return (
    <button
      disabled={backgroundTasks.available() === 'none'}
      onClick={async () => {
        const r = await backgroundTasks.runNow('sync');
        if (!r.ok) alert(`Could not run it (${r.reason}).`);
      }}
    >
      Sync now
    </button>
  );
}

appmintNative.badge appmintNative#

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

مثال

يضبط أو يمسح عدد العناصر غير المقروءة على أيقونة التطبيق في شاشة التشغيل.

القيمة المُعادة: badge.set(count) → Promise<boolean> (false عندما لا توجد إضافة Badge، مثلًا على الويب)؛ badge.clear() → Promise<void>. المتطلبات: لا شيء على Android (يستخدم إضافة Badge المدمجة).

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

export async function syncBadge() {
  const unread = pushInbox().filter((m) => !m.read).length;
  await badge.set(unread);        // 0 or less clears it
}

export async function onInboxOpened() {
  await badge.clear();
}

اعكس عدد غير المقروء عندما تصل رسالة دفع أثناء فتح التطبيق:

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' خارج تطبيق Android. المتطلبات: لا شيء لقائمة الإعدادات.

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

export async function makeReliable() {
  if (batteryOptimization.isIgnoring()) return true;
  const how = await batteryOptimization.request();
  if (how === 'settings_opened') alert('Choose this app in the list and set it to "Not optimised".');
  return false;
}

اعرض الحالة الحالية في صف إعدادات:

import { useState } from 'react';
import { batteryOptimization } from '@/lib/appmintNative';

export function BatteryRow() {
  const [ignoring, setIgnoring] = useState(batteryOptimization.isIgnoring());
  return (
    <button
      disabled={ignoring}
      onClick={async () => {
        const how = await batteryOptimization.request();
        if (how === 'unsupported' || how === 'unavailable') alert('This phone has no setting for it.');
        setIgnoring(batteryOptimization.isIgnoring());   // reads the current answer after the dialog or settings screen
      }}
    >
      {ignoring ? 'Battery limits are off' : 'Allow background work'}
    </button>
  );
}

اطلب الاستثناء قبل بدء مهمة خلفية طويلة، مع keepAlive:

import { batteryOptimization, keepAlive } from '@/lib/appmintNative';

export async function startUpload() {
  if (!batteryOptimization.isIgnoring()) await batteryOptimization.request();
  const r = await keepAlive.start({ title: 'Uploading photos', type: 'dataSync' });
  if (!r.ok) throw new Error(r.reason);
}

appmintNative.bleSensors appmintNative#

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

مثال

مستشعرات اللياقة القياسية عبر Bluetooth - معدل ضربات القلب، وسرعة/إيقاع الدراجة، والقدرة، وسرعة/إيقاع الجري، وبطارية المستشعر - مع أرقام محلَّلة جاهزة، في تطبيق أندرويد.

القيمة المُعادة: كل طريقة تُعيد Promise<void> (وsetWheelCircumference تُعيد void). كل ما عداه يصل كأحداث نافذة appmint:ble مع detail = {kind:'device', address, name, sensors, rssi} | {kind:'state', state, address?, services?} | {kind:'data', type, data} | {kind:'error', error}. المتطلبات: Bluetooth، تفعّله النسخة المبنية تلقائيًا عندما يستخدم الكود bleSensors. في المتصفح (معاينة الويب) لا يوجد جسر حساسات: تُجيب startScan() بـ {kind:'error', error:'not_enabled'}، فعالج هذا الخطأ. لمعرفة ذلك مسبقًا، اختبر can('bluetooth') === 'native' - وليس !== 'none': في Chrome تكون can('bluetooth') هي 'web' (Web Bluetooth)، وهو ما لا يستخدمه bleSensors.

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

type BleDetail = {
  kind: 'device' | 'state' | 'data' | 'error';
  address?: string; name?: string | null; sensors?: string[]; rssi?: number;
  state?: 'scanning' | 'scan_stopped' | 'connecting' | 'connected' | 'ready' | 'disconnected';
  type?: 'heart_rate' | 'speed_cadence' | 'power' | 'running' | 'battery';
  data?: { bpm?: number; speedKmh?: number; cadenceRpm?: number; watts?: number; cadenceSpm?: number; percent?: number };
  error?: string;
};

export function SensorPanel() {
  const [devices, setDevices] = useState<BleDetail[]>([]);
  const [bpm, setBpm] = useState<number | null>(null);
  const [status, setStatus] = useState('');

  useEffect(() => {
    const onBle = (e: Event) => {
      const d = (e as CustomEvent<BleDetail>).detail;
      if (d.kind === 'device') setDevices((list) => [...list, d]);
      else if (d.kind === 'state') setStatus(d.state ?? '');
      else if (d.kind === 'data' && d.type === 'heart_rate') setBpm(d.data?.bpm ?? null);
      else if (d.kind === 'error') {
        setStatus(d.error === 'not_enabled' ? 'Sensors work in the installed app.' : `Bluetooth: ${d.error}`);
      }
    };
    window.addEventListener('appmint:ble', onBle);
    return () => { window.removeEventListener('appmint:ble', onBle); void bleSensors.disconnect(); };
  }, []);

  return (
    <div>
      <button onClick={() => { setDevices([]); bleSensors.setWheelCircumference(2105); void bleSensors.startScan(); }}>
        Find sensors
      </button>
      {devices.map((d) => (
        <button key={d.address} onClick={() => void bleSensors.connect(d.address!)}>
          {d.name ?? 'Sensor'} ({d.sensors?.join(', ')})
        </button>
      ))}
      <p>{status}</p>
      {bpm !== null && <p>{bpm} bpm</p>}
    </div>
  );
}

قيّد الشاشة بالتطبيق المثبّت.

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

export function SensorsEntry() {
  // 'native' only: in Chrome can('bluetooth') is 'web', which bleSensors does not use.
  if (can('bluetooth') !== 'native') return <p>Heart-rate sensors work in the installed app.</p>;
  return <button onClick={() => void bleSensors.startScan()}>Find sensors</button>;
}

أوقف المسح عند أول مستشعر وراقب سرعة الدراجة.

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

type BikeEvent = { kind: string; address?: string; type?: string; data?: { speedKmh?: number } };

export function BikeSpeed() {
  const [kmh, setKmh] = useState<number | null>(null);

  useEffect(() => {
    bleSensors.setWheelCircumference(2096);   // 700x23c
    const onBle = (e: Event) => {
      const d = (e as CustomEvent<BikeEvent>).detail;
      if (d.kind === 'device' && d.address) {
        void bleSensors.stopScan().then(() => bleSensors.connect(d.address!));
      } else if (d.kind === 'data' && d.type === 'speed_cadence') {
        setKmh(d.data?.speedKmh ?? null);
      }
    };
    window.addEventListener('appmint:ble', onBle);
    void bleSensors.startScan();
    return () => { window.removeEventListener('appmint:ble', onBle); void bleSensors.disconnect(); };
  }, []);

  return <p>{kmh === null ? 'Looking for a sensor...' : `${kmh.toFixed(1)} km/h`}</p>;
}

ملاحظات: يستمر البحث 15 ثانية (stopScan() ينهيه مبكرًا)؛ ويتصل مستشعر واحد في كل مرة. رموز الأخطاء: not_enabled، permission_denied، bluetooth_unavailable، bluetooth_off، scan_failed، invalid_address. تحتاج السرعة إلى حزمتين وحجم العجلة (الافتراضي 2096 مم). وتصل الرسائل نفسها أيضًا إلى window.onAppMintBle؛ والاستماع إلى الحدث أبسط. لأي جهاز Bluetooth آخر استخدم navigator.bluetooth.

appmintNative.brightness appmintNative#

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

مثال

يرفع سطوع الشاشة لبطاقة صعود أو رمز QR، ثم يعيد التحكم إلى النظام. للتطبيق الأصلي فقط - لا تستطيع صفحة ويب التحكم في الإضاءة الخلفية.

القيمة المُعادة: get() → Promise<number | null> (من 0 إلى 1 بينما يتجاوز التطبيق المستوى؛ -1 عندما يتبع التطبيق مستوى النظام؛ null على الويب أو عندما لا يمكن قراءته). set(value) → Promise<boolean> (تُقيَّد القيمة بين 0 و1؛ false على الويب). restore() → Promise<void>. المتطلبات: لا شيء.

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

function TicketScreen() {
  useEffect(() => {
    if (can('brightness') === 'none') return;
    void brightness.set(1);
    return () => { void brightness.restore(); };
  }, []);
  return <QrCode />;
}

تذكّر المستوى، ارفع السطوع، ثم أعده.

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

export async function showBoardingPass(render: () => Promise<void>) {
  const before = await brightness.get();   // -1 = following the system, null = not readable here
  const ok = await brightness.set(1);
  try {
    await render();
  } finally {
    if (ok) await brightness.restore();    // returns to the system level
  }
  return before;
}

أبقِ الشاشة ساطعة ومستيقظة أثناء عرض رمز.

import { brightness, keepAwake, can } from '@/lib/appmintNative';
import { useEffect } from 'react';

export function useQrPresenting(active: boolean) {
  useEffect(() => {
    if (!active) return;
    if (can('brightness') !== 'none') void brightness.set(0.95);
    void keepAwake(true);
    return () => { void brightness.restore(); void keepAwake(false); };
  }, [active]);
}

ملاحظات: يغيّر نافذة هذا التطبيق فقط، وليس إعداد سطوع الهاتف، وينتهي التجاوز عند إغلاق التطبيق. تضبط restore() القيمة -1 (اتباع النظام). تقرأ get() تجاوز النافذة، لذا تكون -1 حتى تستدعي set() - وهي ليست مستوى النظام.

appmintNative.browser appmintNative#

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

مثال

يفتح صفحة في تبويب متصفح داخل التطبيق (Chrome Custom Tabs) - للشروط أو صفحات المساعدة أو صفحة تسجيل الدخول - وتبويبًا جديدًا على الويب.

القيمة المُرجعة: open() ← Promise<boolean> (false عندما تعذّر فتح التبويب). close() ← Promise<void>. onClosed(cb) ← دالة لإلغاء الاشتراك؛ وcb تعمل عندما يغلق المستخدم التبويب (في التطبيق فقط). المتطلبات: لا شيء.

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

async function openHelp() {
  const off = browser.onClosed(() => {
    off();
    void refreshAccount();       // the user may have changed something on the site
  });
  const ok = await browser.open('https://example.com/help', { toolbarColor: '#0B0F1A' });
  if (!ok) { off(); showMessage('Could not open the help page.'); }
}

رابط الشروط في مكوّن React.

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

export function TermsLink() {
  return (
    <button
      onClick={async () => {
        if (!(await browser.open('https://example.com/terms'))) toast('Could not open the terms page');
      }}
    >
      Terms of use
    </button>
  );
}

أغلق التبويب من الكود وحدّث عندما يعود المستخدم.

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

export function usePaymentTab(onBack: () => void) {
  useEffect(() => browser.onClosed(onBack), [onBack]);   // the returned function unsubscribes
  return {
    start: (url: string) => browser.open(url),
    cancel: () => browser.close(),   // closes the tab in the app; does nothing on the web
  };
}

ملاحظات: القيمة الافتراضية لـ toolbarColor هي رمز --bg الخاص بالتطبيق. التبويب متصفح منفصل - فهو لا يشارك ملفات تعريف الارتباط أو localStorage الخاصة بالتطبيق. يغلق close() التبويب من الكود داخل التطبيق؛ وعلى الويب لا يفعل شيئًا. لا يُطلق onClosed أبدًا على الويب.

appmintNative.calendar appmintNative#

calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar

مثال

يضيف أحداثًا في تقويم هاتف المستخدم الخاص (مزوّد تقويم Android) ويسردها ويحذفها، ويفتح تطبيق التقويم عند تاريخ.

القيمة المُرجعة: requestPermission() ← Promise<'granted' | 'denied'>. createEvent(input) ← Promise<string | null> (معرّف الحدث؛ وnull عندما رفض المستخدم الوصول إلى التقويم - وتُرفض عندما يتعذّر إنشاء الحدث نفسه، مثل تواريخ غير صالحة). listEvents({ from, to }) ← Promise<{ id, title, start, end, location?, notes?, allDay? }[]> (الأوقات بالميلي ثانية منذ 1970). deleteEvent(id) / openCalendar(date?) ← Promise<boolean>. لا تُرفض إلا createEvent، وفقط لحدث تعذّر إنشاؤه. المتطلبات: فعّل التقويم في الخطوة 4 (الوصول) عند الإنشاء - يضع الإنشاء علامة عليه لك عندما يرى calendar.createEvent( أو calendar.listEvents( أو calendar.requestPermission( في الكود.

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

async function addBooking(b: { name: string; at: Date; minutes: number; place: string }) {
  if ((await calendar.requestPermission()) !== 'granted') {
    showMessage('Allow calendar access to add the booking.');
    return;
  }
  const id = await calendar.createEvent({
    title: `Booking: ${b.name}`,
    start: b.at,
    end: new Date(b.at.getTime() + b.minutes * 60_000),
    location: b.place,
    notes: 'Added by the app',
  });
  if (id) saveEventId(b, id);
}

اعرض هذا الأسبوع، احذف، افتح:

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

if (can('calendarRead') !== 'none') {
  const week = await calendar.listEvents({ from: Date.now(), to: Date.now() + 7 * 86_400_000 });
  if (week[0]) await calendar.deleteEvent(week[0].id);
}
await calendar.openCalendar(new Date());

أضف إلى التقويم فقط عندما تكون الكتابة ممكنة.

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

export async function addReminder(title: string, when: Date) {
  if (can('calendarWrite') === 'none') { toast('Calendar is not available here'); return null; }
  const id = await calendar.createEvent({ title, start: when, end: new Date(when.getTime() + 30 * 60_000) });
  toast(id ? 'Added to your calendar' : 'Calendar access was refused');
  return id;
}

ملاحظات: على الويب ينزّل createEvent ملف .ics يفتحه المستخدم في تقويمه (ويُرجع UID الخاص به)، وتُرجع listEvents [] (وcan('calendarRead') هي 'none')، وتُرجع deleteEvent / openCalendar false. التذكيرات والأحداث المتكررة متاحة في Capacitor.Plugins.Calendar، وليس عبر هذا المغلّف.

appmintNative.camera appmintNative#

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

مثال

يلتقط صورة بالكاميرا أو يختار صورًا من المعرض: مكوّن Capacitor Camera في التطبيق، وحقل ملف مع capture على الويب.

القيمة المُعادة: take() → Promise<Photo | null> مع { dataUrl, mime }؛ null عند الإلغاء أو رفض الإذن. pick() → Promise<Photo[]>، فارغة عند الإلغاء. المتطلبات: Camera في الخطوة 4 (Access) - تفعّله النسخة المبنية عندما يستدعي كودك 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 مفعّل في خطوة Integrate (الخطوة 3) عند الإنشاء.

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

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

اعرض "مسح" فقط عند وجود ما يُمسح، وأبلغ بالنتيجة:

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

export function InboxToolbar({ onChange }: { onChange: () => void }) {
  const count = pushInbox().length;
  if (count === 0) return null;
  return (
    <button
      onClick={() => {
        // false on the web, or in an app built without AppMint Push
        if (clearPushInbox()) { toast('Inbox cleared'); onChange(); }
        else toast('Could not clear the inbox here');
      }}
    >
      Clear all ({count})
    </button>
  );
}

علّم كل شيء كمقروء بدلًا من حذفه، فلا يضيع شيء:

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

export function markAllRead(): number {
  const unread = pushInbox().filter((m) => !m.read).length;
  markPushRead();   // no argument = every message
  return unread;
}

ملاحظات: لا تتأثر إشعارات الدرج ولا سجل إرسالك في AppMint؛ وتظل الرسائل الجديدة تصل.

appmintNative.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() إضافية. Android فقط (من الغلاف WebToApk.playClick).

appmintNative.Clipboard appmintNative#

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

مثال

الحافظة بشكل إضافة Clipboard الخاصة بـ Capacitor - الكود نفسه كما في copy() / paste().

القيمة المُعادة: write() → Promise<void>. read() → Promise<{ value: string; type: 'text/plain' }> (value تكون '' عندما تكون الحافظة فارغة). المتطلبات: لا شيء.

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

async function copyInvite(code: string) {
  await Clipboard.write({ string: code });
}

async function pasteInvite(): Promise<string> {
  if (can('clipboardRead') === 'none') return '';   // hide the Paste button instead
  try {
    const { value } = await Clipboard.read();
    return value;
  } catch {
    return '';   // the browser denied clipboard access
  }
}

انسخ رمزًا وأكّد بإشعار 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
  }
}

ملاحظات: في التطبيق يستخدم إضافة Clipboard الخاصة بـ Capacitor؛ وفي المتصفح navigator.clipboard. يرسل write فقط string (أو url عندما تكون string فارغة) - بدون صور. في المتصفح يمكن أن يرمي read() خطأ عندما يرفض المستخدم الوصول؛ وفي التطبيق تكون الحافظة الفارغة ''، وليست خطأ. اسأل can('clipboardRead') قبل عرض عنصر اللصق.

appmintNative.collapsingHeader appmintNative#

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

مثال

يجعل ترويسة بعنوان كبير تستجيب للتمرير كما تفعل الترويسات الأصلية، بنشر قيمة من 0 → 1 يمكن لـ CSS استخدامها.

القيمة المُعادة: دالة لإلغاء الاشتراك تزيل أيضًا المتغير والسمة. المتطلبات: لا شيء.

يضبط --collapse (من 0 في الأعلى إلى 1 بعد range px من التمرير) على الترويسة، ويضيف data-collapsed عند الانطواء الكامل:

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

export function NotesScreen() {
  const headerRef = useRef<HTMLElement>(null);
  const listRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!headerRef.current || !listRef.current) return;
    return collapsingHeader(headerRef.current, { scroller: listRef.current, range: 80 });
  }, []);

  return (
    <div className="flex h-full flex-col">
      <header ref={headerRef} className="app-header">Notes</header>
      <div ref={listRef} className="flex-1 overflow-y-auto">{/* rows */}</div>
    </div>
  );
}
.app-header { font-size: calc(28px - 10px * var(--collapse, 0)); padding-top: var(--safe-top); }
.app-header[data-collapsed] { box-shadow: 0 1px 0 var(--border); }

أوقف الاستماع من سكربت عادي؛ وهذا يزيل أيضًا --collapse و data-collapsed:

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

const header = document.querySelector<HTMLElement>('.app-header')!;
const stop = collapsingHeader(header);   // whole-page scroll, default 64 px range
addEventListener('pagehide', stop);

ملاحظات:

  • القيمة الافتراضية لـ scroller هي النافذة (صفحة تُمرَّر كاملةً)؛ والقيمة الافتراضية لـ range هي 64 px.
  • مستمع التمرير سلبي ويُحدَّث مرة واحدة لكل إطار حركة، فلا يبطّئ التمرير.

appmintNative.composeSms appmintNative#

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

مثال

يفتح تطبيق المراسلة بالرقم والنص معبّأين؛ ويضغط المستخدم إرسال. يعمل في تطبيق Android (جسر الغلاف) وعلى الويب (رابط sms:).

القيمة المُعادة: boolean فورًا - true عندما طُلب فتح تطبيق المراسلة. المتطلبات: لا إذن.

زر "راسل المتجر".

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

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

خطّاف قابل لإعادة الاستخدام يخفي الزر عندما لا يوجد رقم.

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

export function useTextShop(phone: string) {
  const available = can('smsCompose') !== 'none' && phone.trim() !== '';
  return {
    available,
    send: (orderId: string) => composeSms(phone, `Hi, about order ${orderId}.`),
  };
}

ادعُ صديقًا؛ ويضغط المستخدم إرسال.

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

export function inviteFriend(number: string, link: string) {
  if (!composeSms(number, `Join me: ${link}`)) toast('Could not open the messaging app');
}

ملاحظات: فضّل هذا على sendSms() لكل ما يكتبه المستخدم أو يوافق عليه - لا شيء يغادر الهاتف دون أن يضغط المستخدم إرسال. على الويب ينتقل إلى sms:…?body=…. لا يشغّل استدعاء composeSms( خيار SMS (قراءة وإرسال) عند الإنشاء، فلا يضيف إذنًا مقيَّدًا من Play.

appmintNative.copy appmintNative#

copy(text: string): Promise<boolean>

مثال

يضع نصًا في حافظة النظام.

القيم المُعادة: Promise<boolean> - تكون true عند نسخ النص، وfalse عندما تكون الحافظة غير متاحة أو رفض المتصفح. المتطلبات: لا شيء.

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

export function CopyLink({ link }: { link: string }) {
  if (can('clipboardWrite') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (await copy(link)) { haptic('success'); toast('Link copied'); }
        else toast('Could not copy the link');
      }}
    >
      Copy link
    </button>
  );
}

الشكل بأسلوب Capacitor يفعل الشيء نفسه:

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

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

أبلغ عن النتيجتين، مع ملاحظة لمس عند النجاح فقط:

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

export async function copyCoupon(code: string) {
  const ok = await copy(code);
  if (ok) haptic('success');
  toast(ok ? `Copied ${code}` : 'Copy failed, press and hold to copy');
  return ok;
}

ملاحظات: في التطبيق تكتب إضافة Capacitor Clipboard النص (خطأ الإضافة يرفض الوعد بدلًا من إرجاع false). في المتصفح تستخدم navigator.clipboard.writeText، التي تحتاج صفحة https وعادةً نقرة من المستخدم.

appmintNative.copyImage appmintNative#

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

مثال

ينسخ صورة إلى الحافظة لتُلصق في تطبيقات الدردشة والبريد: حافظة التطبيق في تطبيق Android، وAsync Clipboard API على الويب.

القيمة المُعادة: Promise<boolean> - true بمجرد النسخ. المتطلبات: لا شيء؛ اشترط عنصر التحكم على can('clipboardImage').

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

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

انسخ صورة من الكاميرا، ثم أكّد ذلك بإشعار 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');
}

اعرض الزر فقط حيث تقبل الحافظة الصور، وانسخ صورة من رابط.

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: طراز الهاتف، وإصدار نظام التشغيل، ومعرّف ثابت واللغة. يستخدم إضافة Device الخاصة بـ Capacitor في التطبيق وإجابة المتصفح على الويب.

القيمة المُعادة: 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) واللغة، إضافةً إلى تقرير الجهاز الكامل لغلاف Android داخل تطبيق Android.

القيم المُعادة: DeviceInfo فورًا: دائمًا { platform, language } - platform هي 'android' في التطبيق و'web' في المتصفح. في تطبيق Android يملأ أيضًا model وmanufacturer وosVersion (مثل "14") وappVersion (مثل "1.4.0")، ويحمل كل مفاتيح WebToApk.getDeviceInfo(): android وhardware وscreen وapp وruntime وinstallId. على الويب يضيف osVersion (وكيل المستخدم). المتطلبات: لا شيء.

تفرّع حسب المنصة أثناء العرض.

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

مثال

مربعات حوار نظام حقيقية: رسالة، سؤال نعم/لا، سؤال نصي من سطر واحد. يستخدم التطبيق إضافة Dialog الخاصة بـ Capacitor؛ ويستخدم الويب 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 على الويب. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

export function swipeAway(id: string, setItems: (v: ReturnType<typeof pushInbox>) => void,
                          showUndo: (onUndo: () => void) => void) {
  if (!dismissPushMessage(id)) return;
  setItems(pushInbox());
  showUndo(() => {                         // e.g. a 5-second "Undo" snackbar
    restorePushMessage(id);
    setItems(pushInbox());
  });
}

اعرض صندوق الوارد مع معالج سحب وحالة تراجع، حتى لا يكون السحب نهائيًا أبدًا.

import { useState } from 'react';
import { pushInbox, dismissPushMessage, restorePushMessage } from '@/lib/appmintNative';

export function Inbox() {
  const [items, setItems] = useState(pushInbox());
  const [undoId, setUndoId] = useState<string | null>(null);

  const swipe = (id: string) => {
    if (!dismissPushMessage(id)) return;     // false on the web or when AppMint Push is off
    setItems(pushInbox());
    setUndoId(id);
  };
  const undo = () => {
    if (undoId && restorePushMessage(undoId)) setItems(pushInbox());
    setUndoId(null);
  };

  return (
    <ul>
      {items.map((m) => <li key={m.id} onClick={() => swipe(m.id)}>{m.title}</li>)}
      {undoId && <button onClick={undo}>Undo</button>}
    </ul>
  );
}

أخفِ كل الرسائل المقروءة دفعة واحدة، وأبلغ بعدد ما أُخفي.

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

export function tidyReadMessages(): number {
  const hidden = pushInbox().filter((m) => m.read && dismissPushMessage(m.id)).length;
  toast(hidden ? `${hidden} message(s) hidden` : 'Nothing to hide');
  return hidden;
}

ملاحظات: يتغير صندوق وارد هذا الهاتف فقط؛ ويبقى إشعار شريط الإشعارات وسجل الإرسال.

appmintNative.docScanner appmintNative#

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

مثال

يفتح ماسح المستندات في النظام - اكتشاف الحواف، والتقويم، وعدة صفحات - وهو ماسح Google ML Kit في تطبيق أندرويد. تعود الصفحات بصيغة روابط بيانات JPEG.

القيمة المُرجعة: Promise<{ pages: string[]; pdfPath?: string } | null> - null عندما يلغي المستخدم أو يتعذّر تشغيل الماسح أو على الويب. isSupported() ← Promise<boolean>: هل يستطيع هذا الهاتف المسح (خدمات Google Play)؛ وfalse على الويب. المتطلبات: لا شيء لتفعيله (في أندرويد يعمل الماسح في خدمات Google Play ولا يحتاج إذن الكاميرا).

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

export function ScanButton({ onPages }: { onPages: (p: string[]) => void }) {
  if (can('docScanner') === 'none') return null;     // no web scanner: hide the button
  return (
    <button onClick={async () => {
      const res = await docScanner.scan({ pageLimit: 3, quality: 0.8 });
      if (res) onPages(res.pages);                   // each a data:image/jpeg;base64,… URL
    }}>
      Scan document
    </button>
  );
}

اسأل الهاتف أولًا، لأن جهازًا أقدم قد لا يملك الماسح، ثم احتفظ بالصفحات.

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

export function ScanPanel() {
  const [ready, setReady] = useState(false);
  const [pages, setPages] = useState<string[]>([]);

  useEffect(() => {
    if (can('docScanner') === 'none') return;          // web: no scanner at all
    docScanner.isSupported().then(setReady);           // false without Google Play services
  }, []);

  if (!ready) return <p>Scanning is not available on this phone.</p>;
  return (
    <>
      <button onClick={async () => {
        const res = await docScanner.scan({ pageLimit: 5 });
        if (!res) return;                              // cancelled or could not start
        setPages((p) => [...p, ...res.pages]);
      }}>Scan pages</button>
      {pages.map((src, i) => <img key={i} src={src} alt={`Page ${i + 1}`} width={120} />)}
    </>
  );
}

امسح ضوئيًا، ثم ارفع الصفحة الأولى بأداة نقل الملفات.

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

export async function scanAndUpload(): Promise<boolean> {
  const res = await docScanner.scan({ pageLimit: 1, quality: 0.7 });
  if (!res || res.pages.length === 0) return false;
  const blob = await (await fetch(res.pages[0])).blob();
  const file = new File([blob], 'page-1.jpg', { type: 'image/jpeg' });
  const r = await fileTransfer.upload('https://example.com/api/scans', file, { fieldName: 'scan' });
  return r.responseCode >= 200 && r.responseCode < 300;
}

ملاحظات: pageLimit 0 (الافتراضي) = بلا حد؛ quality من 0.1 إلى 1 (الافتراضي 0.85). مع pdf: true يكون pdfPath مسار ملف في ذاكرة التخزين المؤقت للتطبيق، وليس عنوانًا تستطيع الصفحة تحميله. عناوين البيانات (Data URLs) كبيرة - غيّر حجمها أو ارفعها بدلًا من الاحتفاظ بالكثير منها في localStorage. لصورة عادية استخدم camera.take().

appmintNative.enableNotifications appmintNative#

enableNotifications(): Promise<EnableNotificationsResult>

مثال

يعيد مستخدمًا رفض الإشعارات أو تخطاها، من زر نقر عليه: مربع إذن الإشعارات ما دام يمكن عرضه، وإلا (تطبيق Android) إعدادات إشعارات هذا التطبيق.

القيمة المُعادة: Promise<{ state, changed, openedSettings }> - state هي 'granted' | 'prompt' | 'blocked' | 'unavailable'، وchanged صحيحة عندما تختلف عما قبل الاستدعاء، وopenedSettings صحيحة عندما استُخدمت شاشة الإعدادات. لا يُرفض أبدًا. على الويب يسأل عبر 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})`);
}

نزّل مع حالة تقدّم، ثم افتح الملف، في hook واحد.

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

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

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

ملاحظات: تكون fraction صفرًا طالما لا يرسل الخادم الطول. في التطبيق يُحفظ التنزيل في مجلد ذاكرة التخزين المؤقت للتطبيق (وليس التنزيلات الخاصة بالمستخدم)؛ ولإعطاء المستخدم نسخة استخدم saveFile. وللرفع ينسخ التطبيق الملف أولًا إلى ذاكرته المؤقتة، فالملفات الكبيرة جدًا تحتاج هذه المساحة الحرة. method (الافتراضي 'POST') وfileName خياران اختياريان للرفع.

appmintNative.fileViewer appmintNative#

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

مثال

يفتح مستندًا (PDF أو صورة أو ملف مكتبي) في تطبيق العرض في الهاتف. على الويب يفتح العنوان في تبويب جديد.

القيم المُعادة: Promise<boolean> - true عند فتح العارض، false عند الفشل (لا تطبيق لهذا النوع، مسار تالف) أو عندما لا يمكن فتح الهدف على هذه الواجهة. المتطلبات: لا شيء لتشغيله؛ يستخدم مكوّن Capacitor FileViewer المضمَّن في التطبيق.

افتح مستندًا من الويب:

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

async function openManual() {
  const ok = await fileViewer.open('https://example.com/manual.pdf');
  if (!ok) alert('No app on this phone can open this file.');
}

افتح ملفًا نزّلته أولًا بواسطة fileTransfer.download (مسار محلي في التطبيق):

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

const r = await fileTransfer.download('https://example.com/invoice.pdf', 'invoice.pdf');
if (r.path) await fileViewer.open(r.path);            // in the app
else if (r.blob) console.log('saved by the browser'); // on the web

افتح رابطًا من زر وأخبر المستخدم عندما لا يستطيع أي عارض فتحه.

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

export function ManualButton({ url }: { url: string }) {
  return (
    <button onClick={async () => {
      if (!(await fileViewer.open(url))) toast('No app can open this file');
    }}>
      View manual
    </button>
  );
}

ملاحظات: الهدف من نوع http(s) يستخدم openDocumentFromUrl؛ وأي شيء آخر يُعامل كمسار محلي (تُزال file://). على الويب لا تُفتح إلا أهداف http(s): وblob: وdata:؛ والمسار المحلي يُجيب false.

appmintNative.folder appmintNative#

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

مثال

مجلد كامل يمنح المستخدم الوصول إليه مرة واحدة ويحتفظ به التطبيق عبر عمليات التشغيل، مع عمليات ملفات حقيقية داخله (في التطبيق: جسر المجلدات في الغلاف).

القيمة المُعادة: كل عضو غير متزامن - connected() → boolean، label() → string، request() → boolean (false عند الإلغاء)، disconnect() → void، list(path) → [{ name, uri, isDirectory, length, lastModified }]، readText(path) → string | null (null = لا يوجد ملف كهذا)، writeText(path, text) / mkdir(path) / rename(path, newName) / remove(path, recursive) → boolean، stat(path) → { name, size, mime, lastModified, uri, isDirectory, canWrite } | null. المتطلبات: يفعّل التطبيق المبني بالذكاء الاصطناعي مفتاح Native Folder Access (SAF) تلقائيًا عندما يستخدم الكود folder؛ وفي النسخة المبنية يدويًا فعّله في الخطوة 4 (Access).

اتصل، واعرض المجلد، ثم افصل:

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

export async function setupFolderPanel(el: { name: HTMLElement; connect: HTMLButtonElement; disconnect: HTMLButtonElement }) {
  if (can('folderAccess') === 'none') { el.connect.hidden = true; return; }   // web build, or not shipped

  const refresh = async () => {
    const on = await folder.connected();
    el.name.textContent = on ? `Saving to: ${await folder.label()}` : 'No folder chosen';
    el.connect.hidden = on;
    el.disconnect.hidden = !on;
  };

  el.connect.onclick = async () => { await folder.request(); await refresh(); };   // from a tap only
  el.disconnect.onclick = async () => { await folder.disconnect(); await refresh(); };
  await refresh();
}

اقرأ واكتب واعرض وأعد التسمية واحذف - كل مسار نسبي إلى المجلد الممنوح ('' هو جذره):

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

await folder.mkdir('notes');
const saved = await folder.writeText('notes/today.md', '# Today\n');   // parents created
const text = await folder.readText('notes/today.md');                  // null if missing
const entries = await folder.list('notes');
entries.forEach((e) => console.log(e.isDirectory ? 'dir ' : 'file', e.name, e.length));

const info = await folder.stat('notes/today.md');
if (info) console.log(info.size, 'bytes, writable:', info.canWrite);

await folder.rename('notes/today.md', 'journal.md');                   // a new NAME, not a path
await folder.remove('notes', true);                                    // recursive for a non-empty folder

انسخ ملاحظة احتياطيًا إلى المجلد الممنوح، وقل ذلك بوضوح عندما لا يكون أي مجلد متصلًا.

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

export async function backupNote(name: string, text: string) {
  if (can('folderAccess') === 'none') return;
  if (!(await folder.connected()) && !(await folder.request())) {
    toast('Choose a folder to save backups');
    return;
  }
  const ok = await folder.writeText(`backups/${name}.md`, text);
  toast(ok ? 'Backed up' : 'Could not write to the folder');
}

ملاحظات: can('folderAccess') هي 'none' على الويب - لا يوجد ما يعادل المجلد الدائم في متصفح الهاتف، فلا تعرض الميزة هناك (استخدم pickFile() / saveFile() لملفات مفردة). استدعِ request() فقط من نقرة المستخدم. يُرفض .. والمسارات المطلقة. لا يمكن إزالة المجلد الممنوح نفسه أبدًا. يعمل stat() للملفات والمجلدات الفرعية (للمجلد isDirectory: true وsize: 0)؛ وstat('') - الجذر - هو null، استخدم label() له. يمكن استدعاء request() مجددًا بعد الإلغاء.

appmintNative.fullscreen appmintNative#

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

مثال

يخفي أشرطة النظام (في التطبيق) أو يدخل وضع ملء الشاشة في المتصفح (على الويب)، لفيديو أو لعبة أو عرض قراءة.

القيم المُعادة: يُرجع enter() وexit() القيمة Promise<void> ولا يُرفضان أبدًا؛ ويُرجع isOn() القيمة boolean بشكل متزامن. المتطلبات: لا شيء.

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

export function FullscreenToggle() {
  const [on, setOn] = useState(fullscreen.isOn());
  if (can('fullscreen') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (fullscreen.isOn()) await fullscreen.exit(); else await fullscreen.enter();
        setOn(fullscreen.isOn());
      }}
    >
      {on ? 'Exit full screen' : 'Full screen'}
    </button>
  );
}

اخرج من وضع ملء الشاشة عند إغلاق العرض:

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

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

أخفِ أشرطة النظام في عرض القراءة وتابع الحالة من زر.

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

export function useReaderMode() {
  const [on, setOn] = useState(false);
  useEffect(() => () => { void fullscreen.exit(); }, []);        // always leave on unmount
  const toggle = async () => {
    if (can('fullscreen') === 'none') return;
    if (fullscreen.isOn()) await fullscreen.exit(); else await fullscreen.enter();
    setOn(fullscreen.isOn());                                   // a refused web request leaves it off
  };
  return { on, toggle };
}

ملاحظات:

  • في التطبيق هذا هو وضع ملء الشاشة الخاص بالغلاف (تختفي أشرطة الحالة والتنقل)؛ وهو متاح دائمًا للصفحة.
  • على الويب تعمل Fullscreen API فقط من نقرة المستخدم. الطلب المرفوض يُتجاهل بصمت، لذا اقرأ isOn() بعده بدلًا من الافتراض.

appmintNative.geofences appmintNative#

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

مثال

سياجات جغرافية تعمل والتطبيق مغلق وتستمر بعد إعادة التشغيل: add, remove, list, events, clearEvents, onEvent, available.

القيمة المُرجعة: add(spec) ← Promise<BgResult> تُحلّ بمجرد أن تقبلها خدمات Play أو ترفضها ({ id, ok, reason? } - background_permission_required، location_off، too_many_geofences، …). remove / clearEvents ← Promise<boolean>. list() ← Promise<GeofenceSpec[]>. events() ← Promise<GeofenceEvent[]> (آخر 100، بما فيها ما حدث أثناء إغلاق التطبيق). onEvent(cb) ← إلغاء اشتراك. المتطلبات: السياجات الجغرافية في الإنشاء و"السماح طوال الوقت" (backgroundLocation.requestAlways()).

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

export async function remindAtStore(lat: number, lng: number) {
  let r = await geofences.add({ id: 'store', lat, lng, radiusM: 200, events: ['enter'],
    notify: { title: 'You are near the store', body: 'Your list is ready' } });
  if (!r.ok && r.reason === 'background_permission_required') {
    const p = await backgroundLocation.requestAlways();
    if (p.background === 'granted') r = await geofences.add({ id: 'store', lat, lng, radiusM: 200, events: ['enter'] });
  }
  return r.ok;
}

geofences.onEvent((e) => console.log(e.id, e.event, new Date(e.time)));

اسرد الأسوار الجغرافية النشطة والانتقالات المسجّلة أثناء إغلاق التطبيق.

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

export async function geofenceReport(): Promise<string[]> {
  if (geofences.available() === 'none') return [];      // web build: no geofences
  const active = await geofences.list();
  const events = await geofences.events();              // oldest first, last 100
  return [
    `${active.length} active zone(s)`,
    ...events.map((e) => `${e.id} ${e.event} at ${new Date(e.time).toLocaleTimeString()}`),
  ];
}

أضف منطقة عمل للدخول والخروج، وعالج الرفض بحسب سببه، وأزلها لاحقًا.

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

export async function watchOffice(lat: number, lng: number): Promise<string> {
  const r = await geofences.add({ id: 'office', lat, lng, radiusM: 150, events: ['enter', 'exit'] });
  if (r.ok) return 'Office zone is on';
  if (r.reason === 'location_off') return 'Turn location on to use this';
  if (r.reason === 'too_many_geofences') return 'Remove a zone first';
  return `Could not add the zone (${r.reason ?? 'unknown'})`;
}

export async function stopWatchingOffice() {
  await geofences.remove('office');
  await geofences.clearEvents();
}

appmintNative.Geolocation appmintNative#

Geolocation.getCurrentPosition / watchPosition / clearWatch / checkPermissions / requestPermissions

مثال

شكل @capacitor/geolocation: مكوّن Capacitor Geolocation في التطبيق، وnavigator.geolocation القياسي على الويب. وكلها تُرجع كائن الموقع بحسب W3C.

القيم المُعادة: getCurrentPosition(opts) ← Promise<GeolocationPosition> (يُرفض عند الرفض أو انتهاء المهلة أو عدم وجود موقع)؛ watchPosition(opts, cb) ← Promise<string> (معرّف المراقبة)؛ clearWatch({ id }) ← Promise<void>؛ checkPermissions() / requestPermissions() ← Promise<{ location, coarseLocation }> (كل منها 'granted' | 'denied' | 'prompt'؛ وعلى Android 12+ يمنح مستخدم يختار "تقريبي" coarseLocation فقط). المتطلبات: GPS / الموقع في الخطوة 4 (الوصول) - يشغّل الإنشاء كليهما عندما يستدعي كودك getCurrentPosition أو watchPosition.

أين أنا، مرة واحدة.

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

export async function currentSpot(): Promise<string> {
  try {
    const perm = await Geolocation.checkPermissions();
    if (perm.location !== 'granted') {
      const asked = await Geolocation.requestPermissions();
      if (asked.location !== 'granted') return 'Location is off for this app.';
    }
    const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true, timeout: 15000 });
    return `${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}`;
  } catch {
    return 'Could not get your location. Is location turned on?';
  }
}

تتبّع مسارًا، ثم أوقفه. يتلقى الاستدعاء null وخطأً عندما يفشل تحديد الموقع.

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

let watchId: string | null = null;

export async function startRun(onPoint: (lat: number, lng: number) => void) {
  watchId = await Geolocation.watchPosition({ enableHighAccuracy: true }, (pos, err) => {
    if (!pos) { console.warn('location error', err); return; }
    onPoint(pos.coords.latitude, pos.coords.longitude);
  });
}

export async function stopRun() {
  if (watchId) { await Geolocation.clearWatch({ id: watchId }); watchId = null; }
}

احصل على موقع لمرة واحدة. يُرجع null عندما يرفض المستخدم.

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

export async function nearestCoords(): Promise<{ lat: number; lng: number } | null> {
  try {
    const pos = await Geolocation.getCurrentPosition({ timeout: 10000 });
    return { lat: pos.coords.latitude, lng: pos.coords.longitude };
  } catch {
    return null;                          // refused, timed out or location is off
  }
}

ملاحظات: يعمل navigator.geolocation العادي في التطبيق أيضًا؛ استخدم هذا عندما تريد شكل Capacitor. على Android 12+ قد يسمح المستخدم بالموقع التقريبي فقط - ما زلت تحصل على موقع لكنه أقل دقة. على الويب يرسل requestPermissions() طلب موقع حقيقيًا واحدًا لإظهار نافذة المتصفح. الموقع بيانات شخصية: صرّح به في نموذج أمان البيانات في 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 في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

// Every setPushTag makes the install check in, so skip the calls that change nothing.
export function ensurePushTag(key: string, value: string) {
  if ((getPushTags()[key] ?? '') !== value) setPushTag(key, value);
}

ensurePushTag('plan', 'pro');

اضبط عدة وسوم بتغيير واحد باستخدام setPushTags عندما تغيّر الشاشة أكثر من وسم، وأفرغ القيمة لإزالة وسمها:

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

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

اقرأ الوسوم لتعرض تلميحًا فقط عندما يكون لهذا التثبيت وسم:

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

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

ملاحظات: هذا ما يحتفظ به الهاتف؛ ويراه مركز الإرسال بعد تسجيل الحضور التالي لهذا التثبيت.

appmintNative.getPushUser appmintNative#

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

مثال

يقرأ معرّف المستخدم الذي أعطاه هذا التثبيت آخر مرة لـ AppMint Push بواسطة setPushUser.

القيم المُعادة: string بشكل متزامن - المعرّف المحفوظ، أو '' عند تسجيل الخروج أو عدم ضبطه أو على الويب. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

// On app start: the session expired while the app was closed, so stop personal messages here.
export function syncPushUser(currentUserId: string | null) {
  const want = currentUserId ?? '';
  if (getPushUser() !== want) setPushUser(want);
}

استدعِ setPushUser بمعرّف الحساب بعد تسجيل الدخول وبـ '' عند تسجيل الخروج؛ وتُعيد false عند رفض المعرّف أو إيقاف AppMint Push:

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

export function onSignedIn(userId: string) {
  if (!setPushUser(userId)) console.warn('push user id not saved');   // ids only: no email or phone number
}

export function onSignedOut() {
  setPushUser('');
}

اقرأ المعرّف المحفوظ في React hook، مع وسم يخص ذلك المستخدم:

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

ملاحظات: في التطبيق يستخدم إضافة Haptics الخاصة بـ Capacitor (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 (وليس وعدًا؛ وawait عليها غير ضار). المتطلبات: الاهتزاز في الخطوة 4 (الوصول)؛ وتفعّله لك الإنشاءات بالذكاء الاصطناعي عندما يستخدم الكود Haptics.

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

Haptics.impact({ style: 'LIGHT' });        // 'LIGHT' | 'MEDIUM' | 'HEAVY'  (default 'LIGHT')
Haptics.notification({ type: 'SUCCESS' }); // 'SUCCESS' | 'WARNING' | 'ERROR' (default 'SUCCESS')
Haptics.vibrate({ duration: 300 });        // ms, default 300

منتقٍ يصدر نقرة عند تغيّر القيمة:

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

export function Stepper({ value, onChange }: { value: number; onChange: (v: number) => void }) {
  const step = (d: number) => { Haptics.selectionChanged(); onChange(value + d); };
  return (
    <div onPointerDown={() => Haptics.selectionStart()} onPointerUp={() => Haptics.selectionEnd()}>
      <button aria-label="Less" onClick={() => step(-1)}>-</button>
      <span>{value}</span>
      <button aria-label="More" onClick={() => step(1)}>+</button>
    </div>
  );
}

نموذج يؤكد باهتزاز إشعار ويضيف اهتزازًا قصيرًا عندما يكون حقل خاطئًا:

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

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

ملاحظات: يعطي كل من selectionStart وselectionChanged نقرة اختيار واحدة؛ ولا يفعل selectionEnd شيئًا. يعمل في التطبيق (مكوّن Capacitor Haptics)؛ وعلى الويب يستخدم navigator.vibrate إن توفّر وإلا لا يفعل شيئًا.

appmintNative.has appmintNative#

has(cap: Capability): boolean

مثال

صيغة نعم/لا من can(): صحيحة عندما تقوم الميزة بشيء حقيقي على هذه الواجهة.

القيمة المُرجعة: boolean، بشكل متزامن - بالضبط can(cap) !== 'none'. المتطلبات: لا شيء.

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

export function CopyCode({ code }: { code: string }) {
  if (!has('clipboardWrite')) return <code>{code}</code>;   // no button where it cannot copy
  return (
    <button onClick={async () => { if (await copy(code)) toast('Copied'); }}>
      Copy {code}
    </button>
  );
}

تحكّم في ميزة كاملة:

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

export async function onScanTap(): Promise<string | null> {
  if (!has('codeScanner')) return null;     // better: do not render the Scan button at all
  const hit = await scanCode({ formats: ['qr_code'] });
  return hit ? hit.value : null;
}

اختر تسمية حسب مكان التشغيل: يقرر has() هل ترسم عنصر التحكم، ويخبرك can() بنوعه:

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

export function shareLabel(): string | null {
  if (!has('share')) return null;                      // nothing to share with: draw no button
  return can('share') === 'native' ? 'Share…' : 'Share link';
}

ملاحظات: تأخذ أسماء القدرات نفسها كما في can() (انظر ذلك المدخل للقائمة الكاملة). استخدم can() عندما تحتاج إلى معرفة أين يعمل ('native' أو 'web')، مثلًا لتغيير تسمية.

appmintNative.hideKeyboard appmintNative#

hideKeyboard(): void

مثال

يغلق لوحة المفاتيح على الشاشة - مثلًا عند إرسال نموذج أو فتح ورقة فوق الحقل.

يعيد: لا شيء. يتطلب: لا شيء.

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

export function SearchForm({ onSearch }: { onSearch: (q: string) => void }) {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const q = new FormData(e.currentTarget).get('q') as string;
        hideKeyboard();                       // show the results, not the keyboard
        onSearch(q);
        if (!q) toast('Type something to search');
      }}
    >
      <input name="q" type="search" enterKeyHint="search" aria-label="Search" />
    </form>
  );
}

أغلق لوحة المفاتيح عندما تُفتح ورقة فوق الحقل، حتى لا تختبئ الورقة خلفها:

import { useState } from 'react';
import { hideKeyboard } from '@/lib/appmintNative';

export function useSheet() {
  const [open, setOpen] = useState(false);
  return {
    open,
    show: () => { hideKeyboard(); setOpen(true); },
    hide: () => setOpen(false),
  };
}

أغلقها عندما ينقر المستخدم في أي مكان ليس حقل نص:

import type { ReactNode } from 'react';
import { hideKeyboard } from '@/lib/appmintNative';

export function Screen({ children }: { children: ReactNode }) {
  return (
    <div onPointerDown={(e) => {
      if (!(e.target as HTMLElement).closest('input, textarea')) hideKeyboard();
    }}>
      {children}
    </div>
  );
}

ملاحظات: في التطبيق يطلب من إضافة Keyboard الخاصة بـ Capacitor إخفاء لوحة المفاتيح. على الويب يُزيل التركيز عن الحقل المركَّز، وهذا ما يغلق لوحة المفاتيح في المتصفح. من الآمن استدعاؤه عندما لا تكون لوحة مفاتيح مفتوحة.

appmintNative.inAppCampaigns appmintNative#

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

مثال

الرسائل داخل التطبيق التي يحتفظ بها هذا التثبيت الآن، على شكل {id, name}[]: حملات المنشئ التي يكون هذا المستخدم ضمن جمهورها.

القيمة المُعادة: { id: string; name: string }[] بشكل متزامن؛ و[] على الويب وبدون AppMint Push. المتطلبات: Android: AppMint Push مفعّل في خطوة Integrate (الخطوة 3) عند الإنشاء.

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

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

قائمة تصحيح في شاشة إدارة:

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

export function InAppDebug() {
  const list = inAppCampaigns();
  return <ul>{list.length ? list.map((c) => <li key={c.id}>{c.name}</li>) : <li>None for this install</li>}</ul>;
}

تجاوز تلميحك الخاص عندما يشغّل المنشئ أصلًا رسالة بهذا الاسم:

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

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

appmintNative.installId appmintNative#

installId(): string

مثال

يعطي معرّفًا عشوائيًا ثابتًا لهذا التثبيت: معرّف الغلاف نفسه في تطبيق أندرويد، ومعرّفًا عشوائيًا محفوظًا في 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();
}

استمع (أزل المقبض عند الإزالة):

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

useEffect(() => {
  const h = Keyboard.addListener('keyboardDidShow', ({ keyboardHeight }) => {
    composer.style.paddingBottom = `${keyboardHeight}px`;
  });
  const g = Keyboard.addListener('keyboardDidHide', () => { composer.style.paddingBottom = '0px'; });
  return () => { h.remove(); g.remove(); };
}, []);

اجمع الاثنين لتمرير الحقل المحدد إلى مجال الرؤية عند فتح لوحة المفاتيح، ثم أغلقها عند Enter:

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

export function useKeyboardScroll() {
  useEffect(() => {
    const h = Keyboard.addListener('keyboardDidShow', () => {
      document.activeElement?.scrollIntoView({ block: 'center', behavior: 'smooth' });
    });
    const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter') void Keyboard.hide(); };
    window.addEventListener('keydown', onKey);
    return () => { h.remove(); window.removeEventListener('keydown', onKey); };
  }, []);
}

ملاحظات: keyboardHeight هو الجزء من لوحة المفاتيح الذي ما زال على الصفحة تجنّبه، وليس الارتفاع الخام: في تطبيق Android يعيد الغلاف تحجيم الصفحة للوحة المفاتيح، لذلك تكون 0 هناك أثناء فتحها (يظل حدث الإظهار يُطلق، ويُطلق مجددًا عند تغيّر الرقم). تُطلق أحداث الإخفاء فقط عندما تُغلق لوحة المفاتيح فعلًا، لذا يمكن أن تتتبع "هل لوحة المفاتيح مفتوحة". تُطلق أحداث "Will" و"did" معًا. للحشو فقط، onKeyboard((px) => …) أبسط.

appmintNative.launchUrl appmintNative#

launchUrl(): Promise<string>

مثال

يعطي الرابط الذي أطلق التطبيق أو استأنفه - رابط عميق، اختصار أيقونة، نقرة ودجت، إشعار - ليتمكن التطبيق من فتح الشاشة الصحيحة.

القيم المُعادة: Promise<string> - الرابط الكامل، أو '' عند فتح التطبيق بشكل عادي. دائمًا '' على الويب، حيث يكون شريط العنوان هو المسار أصلًا. المتطلبات: لا شيء للاختصارات ونقرات الودجت؛ أما الروابط الخارجية فتحتاج خيار الروابط العميقة عند الإنشاء.

وجّه بناءً عليه مرة واحدة عند تحميل التطبيق:

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

export function useLaunchRoute() {
  const navigate = useNavigate();
  useEffect(() => {
    void launchUrl().then((u) => {
      if (!u) return;
      const { pathname, search } = new URL(u);
      navigate(pathname + search, { replace: true });
    });
  }, [navigate]);
}

اقرنه مع onDeepLink() للروابط التي تصل بينما التطبيق مفتوح أصلًا:

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

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

تعامل مع السلسلة الفارغة على أنها "فُتح بشكل عادي" واختر شاشة البداية من الرابط فقط عندما يكون هناك رابط. الاختصار مثل appmint-shortcut://<package>/orders يحمل المسار في مساره.

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

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

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

ملاحظات:

  • القيمة تُحفظ ولا تُستهلك: المكوّن الذي يُركَّب متأخرًا يراها أيضًا. الرابط الأحدث يستبدلها.
  • يُقرأ من الغلاف (WebToApk.getLaunchUrl())؛ لا تستدعِ ذلك مباشرة أبدًا في تطبيق مبني بالذكاء الاصطناعي. تصل الاختصارات ونقرات الودجت على شكل appmint-shortcut://<package><route>.
  • حلّل باستخدام new URL(u) وانتقل إلى مساره؛ ولا تحمّل الرابط نفسه.

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 بالثواني. فارغة عندما يكون سجل المكالمات متوقفًا أو يُرفض الإذن. المتطلبات: مفتاح Call log (read) في الخطوة 4 (Access) - تفعّله النسخة المبنية عندما يستدعي كودك هذا - وإذن المستخدم عند أول استخدام.

المكالمات الفائتة في آخر 7 أيام.

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

export async function missedThisWeek() {
  if (can('callLog') === 'none') return null;          // web: not possible
  const since = Date.now() - 7 * 24 * 60 * 60 * 1000;
  try {
    const calls = await listCallLog(500, 0, since);   // a number here; the wrapper converts it
    return calls.filter((c) => c.type === 'missed');
  } catch {
    return [];                                          // 2-minute timeout
  }
}

إجمالي وقت المكالمات لكل جهة اتصال من المكالمات الصادرة والواردة، الأطول أولًا.

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

export async function talkTimeByPerson() {
  if (can('callLog') === 'none') return null;
  const calls = await listCallLog(1000, 0).catch(() => []);
  const totals = new Map<string, number>();
  for (const c of calls) {
    if (c.type !== 'incoming' && c.type !== 'outgoing') continue;
    const who = c.name || c.number || 'Unknown';
    totals.set(who, (totals.get(who) ?? 0) + (c.duration ?? 0));   // seconds
  }
  return [...totals].sort((a, b) => b[1] - a[1]);
}

شاشة تشرح القائمة الفارغة، لأن "لا مكالمات" و"رُفض الإذن" يبدوان متشابهين.

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

export function useCalls() {
  const [calls, setCalls] = useState<Awaited<ReturnType<typeof listCallLog>> | null>(null);
  const supported = can('callLog') !== 'none';
  useEffect(() => {
    if (supported) void listCallLog(50).then(setCalls).catch(() => setCalls([]));
  }, [supported]);
  const hint = !supported ? 'Open the app on your phone to see calls'
    : calls && calls.length === 0 ? 'No calls found. Allow call log access in Settings if you expected some.' : '';
  return { calls: calls ?? [], hint };
}

ملاحظات: يُحصر limit عند 2000 بواسطة التطبيق. القائمة الفارغة لا تميّز بين "لا مكالمات" و"رُفض الإذن". تكون can('callLog') هي 'none' على الويب. سجل المكالمات إذن مقيّد في Google Play: تحتاج إلى إقرار أذونات معتمد في Play Console، وإلا يُرفض التطبيق.

appmintNative.listContacts appmintNative#

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

مثال

يقرأ دفتر العناوين صفحة بعد صفحة. لتطبيق Android فقط، عبر جسر الغلاف.

القيمة المُرجعة: Promise<Contact[]>. مصفوفة فارغة عند عدم وجود جهات اتصال، أو عند رفض الوصول، أو عند عدم تفعيل جهات الاتصال وقت الإنشاء. المتطلبات: مفتاح جهات الاتصال في الخطوة 4 (الوصول) - يفعّله الإنشاء عندما يستدعي كودك هذه الدالة - وإذن المستخدم عند أول استخدام.

شاشة قائمة جهات الاتصال. اعرضها فقط حيث يمكن أن تعمل. كل جهة اتصال هي { id?, name?, phones?: string[], emails?: string[], photoUri? }.

import { listContacts, can, type Contact } from '@/lib/appmintNative';

type Row = { id: string; name: string; phone: string };

function toRow(c: Contact): Row {
  return { id: c.id ?? '', name: c.name ?? '', phone: c.phones?.[0] ?? '' };
}

export async function loadPage(page: number): Promise<Row[] | null> {
  if (can('contactList') === 'none') return null;   // web: show "open the app" instead
  try {
    const contacts = await listContacts(100, page * 100);
    return contacts.map(toRow);
  } catch {
    return [];                                        // 2-minute timeout
  }
}

ابحث في دفتر العناوين كله بقراءته صفحة صفحة في قائمة واحدة.

import { listContacts, can, type Contact } from '@/lib/appmintNative';

export async function findContacts(query: string): Promise<Contact[] | null> {
  if (can('contactList') === 'none') return null;
  const all: Contact[] = [];
  for (let offset = 0; ; offset += 200) {
    const page = await listContacts(200, offset).catch(() => []);
    all.push(...page);
    if (page.length < 200) break;
  }
  const q = query.trim().toLowerCase();
  return all.filter((c) => (c.name ?? '').toLowerCase().includes(q));
}

اقرنه مع sendSms لمراسلة أول رقم لجهة اتصال.

import { sendSms, type Contact } from '@/lib/appmintNative';

export async function invite(c: Contact): Promise<boolean> {
  const phone = c.phones?.[0];
  if (!phone) return false;
  return sendSms(phone, `Hi ${c.name ?? ''}, join me on the app!`);
}

ملاحظات: النتيجة الفارغة لا تميّز بين "لا جهات اتصال" و"رُفض الإذن" - اعرض تلميحًا قصيرًا ("اسمح بجهات الاتصال في الإعدادات") عندما تكون القائمة فارغة. تأتي جهات الاتصال مرتبة من A إلى Z بالاسم؛ ويُحصر limit عند 2000. لشخص واحد استخدم pickContact()، الذي لا يحتاج إذنًا. جهات الاتصال بيانات شخصية: صرّح بها في نموذج أمان البيانات في Play وفي سياسة الخصوصية.

appmintNative.listSms appmintNative#

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

مثال

يقرأ رسائل SMS من الوارد أو المرسل أو المسودات، الأحدث أولًا. تطبيق Android فقط.

القيمة المُرجعة: Promise<SmsMessage[]> - كل { id, address, body, date, read, box }. فارغة عندما تكون الرسائل القصيرة معطّلة، أو الإذن مرفوضًا، أو الصندوق فارغًا. المتطلبات: مفتاح الرسائل القصيرة (قراءة وإرسال) في الخطوة 4 (الوصول) - يفعّله الإنشاء عندما يستدعي كودك هذه الدالة - وإذن المستخدم عند أول استخدام.

اعرض أحدث 20 رسالة من الوارد.

import { listSms, can, type SmsMessage } from '@/lib/appmintNative';

export async function latestInbox(): Promise<SmsMessage[] | null> {
  if (can('smsList') === 'none') return null;          // web: hide the screen
  try {
    const msgs = await listSms('inbox', 20, 0);
    return msgs.map((m) => ({ ...m, body: (m.body ?? '').slice(0, 120) }));
  } catch {
    return [];                                          // 2-minute timeout
  }
}

الرسائل غير المقروءة من مرسل واحد، الأحدث أولًا.

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

export async function unreadFrom(address: string) {
  if (can('smsList') === 'none') return null;
  const msgs = await listSms('inbox', 200, 0).catch(() => []);
  return msgs.filter((m) => m.address === address && !m.read);
}

اقرأ المرسَل والمسودات دفعة واحدة، ورتّبها معًا حسب التاريخ.

import { listSms, type SmsMessage } from '@/lib/appmintNative';

export async function outgoing(): Promise<SmsMessage[]> {
  const [sent, drafts] = await Promise.all([
    listSms('sent', 50, 0).catch(() => []),
    listSms('draft', 50, 0).catch(() => []),
  ]);
  return [...sent, ...drafts].sort((a, b) => (b.date ?? 0) - (a.date ?? 0));
}

ملاحظات: القائمة الفارغة لا تفرّق بين "لا رسائل" و"رفض الإذن". يُقيَّد limit بـ 500 من التطبيق. can('smsList') هي 'none' على الويب. لا تشمل هذه الأداة الرسائل الواردة الجديدة؛ تحصل عليها الصفحات المكتوبة يدويًا من الحدث appmint:sms-received (انظر listSms للجسر الخام). SMS إذن مقيّد في Google Play: تحتاج إلى Permissions Declaration معتمد في Play Console، ولا يُمنح عادةً إلا لتطبيقات SMS الافتراضية.

appmintNative.liveProgress appmintNative#

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

مثال

بطاقة حالة ثابتة وصامتة في مركز إشعارات Android تحدّثها في مكانها - توصيل أو رفع أو مؤقت أو تمرين. start, update, end.

القيم المُعادة: start / update ← Promise<boolean> (false عند عدم التوفر، أو عدم السماح بالإشعارات، أو إيقاف المستخدم فئة "الحالة المباشرة"); end ← Promise<void>. المتطلبات: تطبيق Android، وإذن الإشعارات على Android 13+. على الويب تكون can('liveProgress') هي 'none' - اعرض الحالة نفسها داخل تطبيقك هناك.

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

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

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

مؤقّت قيد التشغيل: يعرض startedAt ساعة تعدّ تصاعديًا من تلك اللحظة.

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

عالج الرفض. يُجيب start بـ false؛ ويقول liveProgress.lastError السبب.

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

export async function startDelivery() {
  const ok = await liveProgress.start({ id: 'order', title: 'Order on its way', text: 'Packing', progress: 0.1, route: '/orders' });
  if (!ok) {
    if (liveProgress.lastError === 'permission') toast('Allow notifications to follow your order here');
    else if (liveProgress.lastError === 'channel_disabled') toast('Live status is turned off in system settings');
  }
  return ok;
}

ملاحظات: يحدّث id نفسه بطاقة واحدة؛ والمعرّفات المختلفة تصنع بطاقات منفصلة (المعرّف الافتراضي '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 أو واجهة التذكير. المتطلبات: Android: Notifications مفعّلة في خطوة Access (الخطوة 4) عند الإنشاء.

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

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

عدة إشعارات في استدعاء واحد، يستبدل كل منها إشعارًا سابقًا له tag.

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

export async function reportBatch(done: number, failed: number) {
  await LocalNotifications.schedule({
    notifications: [
      { title: 'Upload finished', body: `${done} files sent`, tag: 'upload-ok' },
      ...(failed ? [{ title: 'Some files failed', body: `${failed} need a retry`, tag: 'upload-failed' }] : []),
    ],
  });
}

عندما تحتاج إلى معرفة هل ظهر، استخدم notify() بدلًا من ذلك وتفاعل مع الإجابة.

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

export async function remind(text: string) {
  const shown = await notify({ title: 'Reminder', body: text, tag: 'reminder' });
  if (!shown) toast(text);   // notifications are off: tell them in the app
}

ملاحظات: فوري فقط - حقول Capacitor id وschedule وat وactionTypeId وextra غير مدعومة؛ كل عنصر هو { title, body?, icon?, tag? }. لإشعار في وقت لاحق تستخدم الصفحة المكتوبة يدويًا WebToApk.scheduleNotificationEx (Android). للأزرار استخدم notifyWithActions.

appmintNative.lockOrientation appmintNative#

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

مثال

يثبّت الشاشة في اتجاه واحد لعرض لا معنى له إلا بهذا الاتجاه - لوحة لعبة، أو مخطط عريض، أو لوحة توقيع. 'any' تحرر القفل.

القيمة المُعادة: Promise<boolean> - true عندما طُبّق القفل (أو إلغاؤه)، false عندما رفضت المنصة. لا يُرفض أبدًا. المتطلبات: لا شيء.

اقفل أثناء فتح العرض، وحرّر عند المغادرة:

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

export function ChartScreen() {
  useEffect(() => {
    if (can('orientationLock') === 'none') return;
    void lockOrientation('landscape');
    return () => { void lockOrientation('any'); };   // always give the rotation back
  }, []);
  return <div className="h-full w-full">{/* the chart */}</div>;
}

تحقق من الإجابة عندما يعتمد العرض عليها:

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

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

حرّر القفل عند زوال الشاشة، كـ hook تشترك فيه عدة شاشات.

import { useEffect } from 'react';
import { lockOrientation, type OrientationLock } from '@/lib/appmintNative';

export function useOrientation(mode: OrientationLock) {
  useEffect(() => {
    void lockOrientation(mode);
    return () => { void lockOrientation('any'); };
  }, [mode]);
}

// useOrientation('landscape') inside the game screen

ملاحظات:

  • في التطبيق يطبّق مكوّن Capacitor ScreenOrientation القفل.
  • على الويب يستخدم screen.orientation.lock(). ترفضه معظم المتصفحات خارج وضع ملء الشاشة وترفضه متصفحات سطح المكتب دائمًا، فتوقّع false هناك.
  • امنح المستخدم دائمًا مخرجًا من عرض مقفل.

appmintNative.longPress appmintNative#

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

مثال

ينادي كودك عندما يضغط المستخدم بإصبعه مطولًا على عنصر - لقائمة سياق صف أو ردود فعل أو إجراءات بطاقة.

القيم المُعادة: دالة إلغاء اشتراك - استدعها عند إزالة المكوّن. المتطلبات: لا شيء.

الخيارات: ms - مدة الضغط، الافتراضي 450؛ haptics - اهتزاز متوسط عند التفعيل، الافتراضي true.

افتح ورقة إجراءات عند الضغط المطوّل، مع زر ظاهر للقائمة نفسها:

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

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

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

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

ضغط أبطأ، بدون اهتزاز:

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

declare const card: HTMLElement;
const off = longPress(card, (e) => console.log('held at', e.clientX, e.clientY), { ms: 700, haptics: false });

اقرنه بـ haptic لتأكيد مخصص، ونظّف لكل صف في القائمة.

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

export function SelectableRow({ id, onSelect, children }: { id: string; onSelect(id: string): void; children: React.ReactNode }) {
  const ref = useRef<HTMLLIElement>(null);
  useEffect(() => {
    if (!ref.current) return;
    return longPress(ref.current, () => { haptic('heavy'); onSelect(id); }, { haptics: false });
  }, [id, onSelect]);
  return <li ref={ref}>{children}</li>;
}

ملاحظات:

  • يُطلق مرة واحدة لكل ضغطة مطولة ويبتلع النقرة التي ستتبعها، فلا تُعدّ الضغطة المطولة "نقرة" أيضًا.
  • تحريك الإصبع أكثر من 8 بكسل يلغيه، فلا يؤدي التمرير إلى تشغيله أبدًا. كما يمنع قائمة الضغط المطوّل الخاصة بالمتصفح على ذلك العنصر.
  • الضغط المطوّل غير مرئي. أبقِ دائمًا زرًا مرئيًا للإجراء نفسه.

appmintNative.markPushRead appmintNative#

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

مثال

يعلّم رسالة واحدة من صندوق AppMint Push كمقروءة، أو كل الرسائل عند الاستدعاء بدون وسيط.

القيمة المُرجعة: boolean بشكل متزامن - true إذا تغيّر شيء. تكون دائمًا false على الويب. المتطلبات: أندرويد: تفعيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

export function openMessage(id: string) {
  markPushRead(id);
  return pushInbox().find((m) => m.id === id);
}

export function markAllRead() {
  markPushRead();          // no id = all messages
  return pushInbox();
}

احسب الرسائل غير المقروءة بعد تعليم واحدة، لشارة في ترويستك الخاصة.

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

export function openAndCount(id: string) {
  const changed = markPushRead(id);          // false on the web or when nothing changed
  const unread = pushInbox().filter((m) => !m.read).length;
  return { changed, unread };
}

علّم كل شيء كمقروء عندما يفتح المستخدم شاشة صندوق الوارد، وحدّث عندما تصل رسالة جديدة.

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

export function useInbox() {
  const [items, setItems] = useState(() => pushInbox());
  useEffect(() => {
    markPushRead();
    const refresh = () => setItems(pushInbox());
    window.addEventListener('appmint:push', refresh);
    return () => window.removeEventListener('appmint:push', refresh);
  }, []);
  return items;
}

ملاحظات: حالة القراءة تبقى على هذا الهاتف فقط؛ ولا يُزال الإشعار من الدرج.

appmintNative.mediaSession appmintNative#

mediaSession — @/lib/appmintNative

مثال

عناصر تحكم التشغيل على شاشة القفل وفي الإشعار لتطبيق يشغّل صوتًا أو فيديو: انشر ما يُشغَّل، واستقبل أزرار تشغيل / إيقاف مؤقت / التالي / السابق / إيقاف، ومرّر فيديو إلى تلفاز.

القيمة المُرجعة: setMetadata وsetPlaybackState وclear ← void. onAction(handler) ← دالة لإلغاء الاشتراك. cast(url) ← Promise<boolean> (false عندما لم يمكن تقديم شيء). المتطلبات: إشعار الوسائط في الخطوة 4 (الوصول) عند الإنشاء؛ وتفعّله لك الإنشاءات بالذكاء الاصطناعي عندما يستخدم الكود mediaSession.

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

type Track = { url: string; title: string; artist: string; cover: string };

export function Player({ track, onNext, onPrev }: { track: Track; onNext: () => void; onPrev: () => void }) {
  const audio = useRef<HTMLAudioElement>(null);
  const unsubscribe = useRef<(() => void) | null>(null);

  const publish = () => {
    const a = audio.current;
    if (!a) return;
    mediaSession.setPlaybackState({
      playing: !a.paused,
      positionMs: a.currentTime * 1000,
      durationMs: (a.duration || 0) * 1000,
    });
  };

  const onPlay = () => {
    // Register the buttons when playback starts, not on mount (see Notes).
    if (!unsubscribe.current) {
      unsubscribe.current = mediaSession.onAction((action) => {
        const a = audio.current;
        if (!a) return;
        if (action === 'play') void a.play();
        if (action === 'pause') a.pause();
        if (action === 'next') onNext();
        if (action === 'prev') onPrev();
        if (action === 'stop') { a.pause(); mediaSession.clear(); }
      });
    }
    mediaSession.setMetadata({ title: track.title, artist: track.artist, artworkUrl: track.cover });
    publish();
  };

  useEffect(() => () => { unsubscribe.current?.(); mediaSession.clear(); }, []);

  return <audio ref={audio} src={track.url} controls onPlay={onPlay} onPause={publish} onSeeked={publish} />;
}

أرسل الفيديو الحالي إلى تلفزيون:

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

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

امسح بطاقة شاشة القفل عند انتهاء التشغيل، واتركها كما هي عندما لا تملك المنصة عناصر تحكم بالوسائط.

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

export function publishEnded(audio: HTMLAudioElement): () => void {
  if (can('mediaControls') === 'none') return () => {};
  const ended = () => {
    mediaSession.setPlaybackState({ playing: false, positionMs: 0, durationMs: audio.duration * 1000 || 0 });
    mediaSession.clear();
  };
  audio.addEventListener('ended', ended);
  return () => audio.removeEventListener('ended', ended);
}

ملاحظات:

  • في أندرويد تصل الأزرار عبر navigator.mediaSession الخاصة بالتطبيق، والمثبّتة عند اكتمال تحميل الصفحة. يضيع onAction المسجَّل قبل ذلك (مثلًا في تأثير mount)، لذا سجّله عند أول تشغيل.
  • يُسلَّم الإجراء seek (مع positionMs) على الويب فقط؛ ولا يملك إشعار Android إمكانية التقديم.
  • artworkUrl يجب أن يكون عنوان https:// كاملًا على الإنترنت. على Android يفتح cast(url) نافذة اختيار "Cast to..." (تمرير؛ ينتهي بـ true بمجرد طلب النافذة). على الويب يستخدم Remote Playback API.

appmintNative.Motion appmintNative#

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

مثال

شكل @capacitor/motion فوق الحدثين القياسيين devicemotion وdeviceorientation: حركة الهاتف وميلانه. يعمل الكود نفسه في التطبيق وفي المتصفح.

القيمة المُعادة: Promise<{ remove(): Promise<void> }> - استدعِ remove() للإيقاف. يستدعي 'accel' مع { acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }؛ و'orientation' مع { alpha, beta, gamma } (درجات). المتطلبات: لا شيء.

هزّ للتحديث.

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

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

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

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

أمِل فقاعة ميزان بحدث الاتجاه.

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

const tilt = await Motion.addListener('orientation', (e) => {
  const { beta, gamma } = e as OrientationEvent;       // front-back, left-right
  bubble.style.transform = `translate(${gamma * 2}px, ${beta * 2}px)`;
});
// later: await tilt.remove();

شكل hook مع تنظيف تلقائي لمكوّن React يقرأ حركة الهاتف.

import { useEffect, useState } from 'react';
import { Motion, can, type AccelEvent } from '@/lib/appmintNative';

export function useAcceleration() {
  const [g, setG] = useState({ x: 0, y: 0, z: 0 });
  const [supported] = useState(() => can('motion') !== 'none');
  useEffect(() => {
    if (!supported) return;
    let handle: { remove(): Promise<void> } | undefined;
    let cancelled = false;
    void Motion.addListener('accel', (e) => setG((e as AccelEvent).accelerationIncludingGravity)).then((h) => {
      if (cancelled) void h.remove(); else handle = h;
    });
    return () => { cancelled = true; void handle?.remove(); };
  }, [supported]);
  return { g, supported };
}

ملاحظات: بعض المتصفحات تطلب إذن الحركة أولًا، لذا ابدأه من نقرة هناك؛ وإذا رفض المستخدم يُعاد المقبض لكن لا تصل أحداث. can('motion') هي 'none' حيث لا يملك المتصفح DeviceMotionEvent. قيم الحساس المفقودة تصل كـ 0. استدعِ remove() دائمًا عند إغلاق الشاشة - تُطلق أحداث الحركة عدة مرات في الثانية.

appmintNative.musicVolume appmintNative#

musicVolume() — @/lib/appmintNative

مثال

يقرأ مستوى الموسيقى الخلفية المتكررة للتطبيق، من 0 إلى 100.

القيم المُعادة: number، بشكل متزامن: 0-100، أو -1 عندما لا توجد موسيقى (على الويب، أو في تطبيق أُنشئ بدون ملف موسيقى). المتطلبات: ملف مختار ضمن الموسيقى الخلفية في الخطوة 3 (التكامل) عند الإنشاء.

ارسم شريط تمرير للموسيقى فقط عندما تكون هناك موسيقى:

import { useState } from 'react';
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export function MusicSetting() {
  const [level, setLevel] = useState(() => musicVolume());
  if (level < 0) return null;                  // no music in this app: no control

  return (
    <label>
      Music
      <input
        type="range" min={0} max={100} value={level}
        onChange={(e) => {
          const v = Number(e.target.value);
          setLevel(v);
          setMusicVolume(v);
        }}
      />
    </label>
  );
}

اجعل المستوى متوافقًا مع مفتاح "كتم الموسيقى"، مع تذكّر مستوى الصوت للعودة إليه.

import { useRef, useState } from 'react';
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export function MuteMusicSwitch() {
  const before = useRef(Math.max(musicVolume(), 50));
  const [muted, setMuted] = useState(() => musicVolume() === 0);
  if (musicVolume() < 0) return null;            // no music in this build

  const toggle = () => {
    if (muted) setMusicVolume(before.current);
    else { before.current = musicVolume(); setMusicVolume(0); }
    setMuted(!muted);
  };
  return <button onClick={toggle}>{muted ? 'Unmute music' : 'Mute music'}</button>;
}

اخفض صوت الموسيقى أثناء تشغيل فيديو، ثم أعده.

import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export function duckMusicDuring(video: HTMLVideoElement) {
  const level = musicVolume();
  if (level < 0) return;                         // nothing to duck
  video.addEventListener('play', () => setMusicVolume(0));
  video.addEventListener('pause', () => setMusicVolume(level));
  video.addEventListener('ended', () => setMusicVolume(level));
}

ملاحظات: لتطبيق Android فقط؛ وتُجيب الويب بـ -1. المستوى المضبوط بـ setMusicVolume يُحفظ للتشغيل التالي؛ وإلى أن تضبط الصفحة واحدًا يكون مستوى الإنشاء.

appmintNative.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 ثانية. استمر في عرض مؤشرك أو هيكلك الخاص؛ فالشريط إضافة.

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 ثانية. استمر في عرض مؤشرك أو هيكلك الخاص؛ فالشريط إضافة.

appmintNative.Network appmintNative#

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

مثال

حالة الاتصال/عدم الاتصال بشكل إضافة Network في Capacitor - الكود نفسه الذي networkStatus() / onNetworkChange().

القيم المُعادة: getStatus() ← Promise<{ connected: boolean; connectionType: string }>. addListener(event, handler) ← شكل Capacitor: وعد (Promise) بمقبض له أيضًا remove() بنفسه؛ ويحصل المعالج على الشكل نفسه عند كل تغيير. المتطلبات: لا شيء.

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

function useOnline() {
  const [online, setOnline] = useState(true);
  useEffect(() => {
    void Network.getStatus().then((s) => setOnline(s.connected));
    const h = Network.addListener('networkStatusChange', (s) => setOnline(s.connected));
    return () => h.remove();
  }, []);
  return online;
}

اقرأ النوع الحالي مرة واحدة واعرضه في شريحة حالة.

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

export function ConnectionChip() {
  const [label, setLabel] = useState('Checking...');
  useEffect(() => {
    void Network.getStatus().then((s) =>
      setLabel(s.connected ? (s.connectionType === 'cellular' ? 'Mobile data' : 'Wi-Fi') : 'Offline'));
  }, []);
  return <span className="chip">{label}</span>;
}

أجّل تنزيلًا كبيرًا على بيانات الجوال، وعالج حالة عدم الاتصال أيضًا.

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

export async function startBigDownload(download: () => Promise<void>) {
  const s = await Network.getStatus();
  if (s.connectionType === 'none' || !s.connected) { toast('You are offline.'); return; }
  if (s.connectionType === 'cellular' && !window.confirm('Download over mobile data?')) return;
  await download();
}

ملاحظات: اسم الحدث يُتجاهل - يحصل كل مستمع على تغييرات الاتصال. connectionType هو 'wifi' أو 'cellular' أو 'none' أو 'unknown': في التطبيق يستخدم getStatus() والمستمع مكوّن Network، وعلى الويب navigator.onLine / navigator.connection.type. تعني connected أن الهاتف لديه شبكة لا أن خادمك يُجيب.

appmintNative.networkStatus appmintNative#

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

مثال

يقول، الآن، ما إذا كان الجهاز متصلًا بالإنترنت.

القيمة المُرجعة: { connected, type }، بشكل متزامن. type هو 'wifi' أو 'cellular' أو 'none' (بلا اتصال) أو 'unknown'. المتطلبات: لا شيء.

خفيف بما يكفي لاستدعائه أثناء العرض:

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

export function SyncButton({ onSync }: { onSync: () => void }) {
  const { connected } = networkStatus();
  return (
    <button disabled={!connected} onClick={onSync}>
      {connected ? 'Sync now' : 'Offline — will sync later'}
    </button>
  );
}

اقرأ القيمة الابتدائية ثم تابع التغييرات:

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

export function useOnline(): boolean {
  const [online, setOnline] = useState(() => networkStatus().connected);
  useEffect(() => onNetworkChange((s) => setOnline(s.connected)), []);
  return online;
}

اختر حجم صورة أخف على بيانات الجوال، مع الإجابة أيضًا عن حالة 'none' (بلا اتصال).

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

export function imageUrl(id: string): string {
  const { connected, type } = networkStatus();
  if (!connected) return '/img/placeholder.svg';         // offline: use the packaged picture
  return `/api/images/${id}?size=${type === 'cellular' ? 'small' : 'large'}`;
}

ملاحظات:

  • داخل التطبيق تكون الإجابة من مكوّن Network (محدَّثة في الخلفية، لذا قد تقرأ أول مكالمة قيمة WebView نفسه). على الويب هي navigator.onLine مع navigator.connection.type حيث يوفّرها المتصفح، وإلا 'unknown' - فلا يُعاد أبدًا تصنيف سرعة مثل '4g' على أنه النوع.
  • "متصل" تعني أن الجهاز لديه شبكة، لا أن خادمك يُجيب. عالج الطلبات الفاشلة على أي حال.

appmintNative.nfc appmintNative#

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

مثال

دوال مساعدة باستدعاء واحد فوق Web NFC (NDEFReader): فحص الدعم وقراءة الوسوم وكتابة وسم نصي. يستخدم تطبيق Android NDEFReader الخاص به؛ وعلى الويب هو Web NFC الخاص بـ Chrome (Chrome على Android).

القيم المُعادة: isSupported(): Promise<boolean>؛ scan(onTag): Promise<() => Promise<void>> (دالة الإيقاف)، ويحصل onTag على Array<{recordType, text, mediaType?}> لكل وسم - أسماء أنواع Web NFC ('text' و'url' و'mime' …) والنص المفكوك على كل منصة؛ write(text): Promise<boolean> (true بعد كتابة وسم، وfalse عند أي فشل). المتطلبات: على Android شغّل دعم Web NFC في الخطوة 4 (الوصول) عند الإنشاء. يُلقي scan() استثناءً عند عدم وجود NDEFReader - افحص can('nfc') أولًا.

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

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

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

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

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

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

اكتب رابطًا إلى وسم، وأخبر المستخدم لماذا فشل ذلك عندما يفشل.

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

export async function writeShareTag(url: string) {
  if (can('nfc') === 'none') { toast('This device cannot write NFC tags.'); return; }
  toast('Hold a blank tag to the back of the phone...');
  const ok = await nfc.write(url);
  toast(ok ? 'Tag written.' : 'Could not write the tag. Try again.');
}

اقرأ وسمًا واحدًا، ثم أوقف المسح.

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

export function readOnce(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    let stop: (() => Promise<void>) | undefined;
    let done = false;
    nfc.scan((records) => {
      if (done) return;
      done = true;
      void stop?.();
      resolve(records.map((r) => r.text).join(' '));
    }).then((s) => { stop = s; if (done) void s(); }, reject);
  });
}

ملاحظات: في أندرويد تكون isSupported() هي true كلما كان الإنشاء يتضمن NFC - ولا تخبر ما إذا كان NFC مفعّلًا (أما window.AndroidNFC.startScan() فتخبر). بعد دالة الإيقاف لا يُستدعى onTag مجددًا أبدًا. في أندرويد يواصل التطبيق نفسه التقاط الوسوم أثناء وجوده في المقدمة (فلا تفتح النقرة تطبيقًا آخر)، لكنه يتوقف عن تمريرها إليك. لا يحتوي text سجل النص على بادئة لغة؛ وtext سجل الرابط هو الرابط الكامل. write() تنتظر حتى تُنقر على وسم. لكتابة سجلات الروابط أو mime ولقفل وسم، استخدم NDEFReader مباشرة.

appmintNative.notify appmintNative#

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

مثال

يعرض إشعارًا واحدًا الآن من تطبيق مبني بالذكاء الاصطناعي، في تطبيق Android وعلى الويب.

القيمة المُعادة: Promise<boolean> - true عند النشر، false عندما رفض المستخدم إذن الإشعارات، أو كانت الإشعارات متوقفة للتطبيق، أو لم تكن متاحة هنا. يطلب الإذن بنفسه في المرة الأولى (Android 13 أو أحدث، والويب). المتطلبات: Android: Notifications مفعّلة في خطوة Access (الخطوة 4) عند الإنشاء.

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

export async function onOrderReady(orderId: string) {
  if (can('notify') === 'none') return;          // hide the feature instead
  const ok = await notify({
    title: 'Order ready',
    body: `Order #${orderId} is ready to collect`,
    icon: 'https://example.com/img/order.png',    // shown beside the text
    tag: `order-${orderId}`,                     // same tag replaces the old one
  });
  if (!ok) setBanner('Turn on notifications to get order alerts.');
}

اسأل في لحظة مناسبة: يعرض أول notify() نافذة الإذن. وللسؤال من زرك "شغّل التنبيهات" - قبل نشر أي شيء - استخدم الواجهة القياسية التي يدعمها التطبيق أيضًا:

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

افتح الشاشة الصحيحة عندما ينقر المستخدم الإشعار، باقتران notify مع onNotificationClick.

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

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

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

ملاحظات: يستخدم Android جسر الإشعارات في الغلاف (تصبح icon الأيقونة الكبيرة؛ استخدم صورة https:// أو data:)؛ وتستخدم الويب واجهة Notification. بعد أن يرفض المستخدم نهائيًا، يُحلّ notify() بـ false دون السؤال مجددًا - وجّهه إلى إعدادات الهاتف. للأزرار استخدم notifyWithActions؛ ولبطاقة تقدّم ثابتة استخدم liveProgress.

appmintNative.notifyWithActions appmintNative#

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

مثال

يعرض إشعارًا بما يصل إلى 3 أزرار ("تحديد كمنجز"، "غفوة"). تعود النقرة عبر onNotificationAction.

القيمة المُعادة: Promise<boolean> - true عند النشر، false عند رفض إذن الإشعارات (يطلبه في المرة الأولى، مثل notify()). المتطلبات: Android: Notifications مفعّلة في خطوة Access (الخطوة 4) عند الإنشاء، وإذن المستخدم على Android 13 أو أحدث. على الويب تكون can('notificationActions') 'none'.

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

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

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

انتهى مؤقّت: زر يضيف خمس دقائق، والآخر يوقفه.

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

export async function timerDone() {
  if (can('notificationActions') === 'none') return false;
  return notifyWithActions({
    title: 'Timer finished',
    tag: 'timer',
    actions: [{ id: 'more', title: '+5 min' }, { id: 'stop', title: 'Stop' }],
  });
}

export const stopTimerButtons = onNotificationAction((e) => {
  if (e.tag !== 'timer') return;                 // ignore other notifications' buttons
  if (e.actionId === 'more') startTimer(5);
});

عالج الإذن المرفوض بعد الاستدعاء، ونظّف داخل المكوّن.

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

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

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

ملاحظات: يعرض كل زر title الخاص به؛ وid الخاص به هو ما يعود كـ e.actionId. خيار input (مربع الرد المضمّن) غير مدعوم في هذا التطبيق - يُتجاهل، ولا يملك الزر مربع رد وتكون e.text دائمًا ''. لا تصل النقرة إلى الصفحة إلا أثناء عمل التطبيق. على الويب ستنشر الأداة إشعارًا عاديًا بلا أزرار، ولهذا يتحقق المثال من can() أولًا.

appmintNative.offlineBanner appmintNative#

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

مثال

يركّب شريطًا جاهزًا ينزلق تحت شريط الحالة عندما ينقطع الاتصال، ويقول "عدتَ متصلًا" لوهلة عندما يعود.

القيمة المُعادة: دالة لإلغاء الاشتراك تزيل الشريط وتوقف الاستماع. المتطلبات: لا شيء.

ركّبه مرة واحدة عند بدء التطبيق:

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

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

كلماتك الخاصة (مثلًا بلغة التطبيق):

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

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

ركّب اللافتة فقط لمنطقة التطبيق المسجَّل دخول المستخدم إليها، وأزلها عند تسجيل الخروج.

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

export function useOfflineBanner(signedIn: boolean) {
  useEffect(() => {
    if (!signedIn) return;
    return offlineBanner({ message: 'Offline: changes will sync later' });
  }, [signedIn]);
}

ملاحظات:

  • الافتراضيات: "أنت غير متصل" و"عدت متصلًا". تختفي رسالة الاتصال بعد نحو 1.8 ثانية؛ وتبقى رسالة عدم الاتصال حتى يعود الاتصال.
  • إذا كان الجهاز غير متصل أصلًا عند التركيب، يظهر الشريط فورًا.
  • مبني على onNetworkChange()، فهو حقيقي على كل سطح. تأتي الألوان من رموز التطبيق (--ink / --bg بلا اتصال، --accent / --accent-ink متصل). ويحتوي على role="status" لقارئات الشاشة.
  • ركّبه مرة واحدة. استدعاءان يرسمان لافتتين.

appmintNative.onAlarm appmintNative#

onAlarm(cb) — @/lib/appmintNative

مثال

يستمع لنتائج المنبّه: التطبيق الذي فتحه منبّه (action: 'open'، مع ringing)، وكيف انتهى الرنين - dismiss أو snooze أو missed (انتهى الرنين أو استُبدل بمنبّه آخر) أو error. تُسلَّم النتائج التي حدثت أثناء إغلاق التطبيق عند التحميل التالي للصفحة.

القيمة المُرجعة: دالة توقف الاستماع. تحصل دالة الاستدعاء على { id, action, ringing?, source?, snoozeMinutes?, snoozedUntil?, error?, soundError? }. المتطلبات: 🔔 منبّه الساعة في خطوة الوصول (الخطوة 4) عند الإنشاء.

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

export function AlarmHost() {
  const [ringingId, setRingingId] = useState<string | null>(null);
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open' && e.ringing) setRingingId(e.id);
    if (e.action === 'dismiss' || e.action === 'snooze' || e.action === 'missed') setRingingId(null);
  }), []);
  return ringingId ? <RingScreen id={ringingId} onClose={() => setRingingId(null)} /> : null;
}

اضبط منبّهًا، مع الإجابة على alarmSupport() أولًا.

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

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

أوقف المنبّه الراني أو اغفِه من شاشتك الخاصة، وتفاعل مع النتيجة.

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

export function RingControls() {
  const [ringing, setRinging] = useState(() => isAlarmRinging());
  const [last, setLast] = useState('');
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open') setRinging(!!e.ringing);
    else { setRinging(false); setLast(e.action === 'error' ? `Alarm error: ${e.error}` : `Alarm ${e.action}`); }
  }), []);
  if (!ringing) return <p>{last}</p>;
  return (
    <div>
      <button onClick={() => stopAlarm()}>Stop</button>
      <button onClick={() => snoozeAlarm('', 5)}>Snooze 5 min</button>
    </div>
  );
}

ملاحظات: التذكيرات العادية التي تفتح التطبيق تصل إلى هنا أيضًا مع action: 'open' و ringing: false.

appmintNative.onBackGesture appmintNative#

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

مثال

يتيح لورقة أو نافذة منبثقة أو عارض صور أو خطوة معالج مفتوحة أن تردّ أولًا على إيماءة الرجوع - السحب من الحافة أو زر الرجوع في Android داخل التطبيق، وزر الرجوع في المتصفح على الويب.

القيمة المُعادة: دالة لإلغاء الاشتراك - استدعِها عند إغلاق العرض أو إزالته. يُعيد معالجك true لاستهلاك الضغطة (أغلقت شيئًا) أو false للسماح بتمريرها. المتطلبات: لا شيء.

سجّل فقط أثناء فتح الورقة:

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

export function FilterPanel() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!open) return;
    return onBackGesture(() => { setOpen(false); return true; });   // back closes THIS, not the app
  }, [open]);

  return (
    <>
      <button onClick={() => setOpen(true)}>Filters</button>
      {open && <div role="dialog" aria-label="Filters">{/* … */}</div>}
    </>
  );
}

ارجع خطوة خطوة عبر معالج، ودع زر الرجوع في الخطوة الأخيرة يغادر بشكل طبيعي:

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

export function useWizardBack(step: number, setStep: (n: number) => void) {
  useEffect(() => onBackGesture(() => {
    if (step === 0) return false;       // not ours: normal back navigation happens
    setStep(step - 1);
    return true;
  }), [step, setStep]);
}

أغلق معرض صور بإيماءة الرجوع، مع ربط التسجيل بكون الصورة مفتوحة.

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

export function Lightbox({ src, onClose }: { src: string | null; onClose: () => void }) {
  useEffect(() => {
    if (!src) return;
    return onBackGesture(() => { onClose(); return true; });
  }, [src, onClose]);
  return src ? <img src={src} alt="" onClick={onClose} /> : null;
}

ملاحظات:

  • تشكّل المعالجات مكدّسًا: يُسأل المعالج المسجَّل أخيرًا (الورقة العلوية) أولًا. والمعالج الذي يطلق خطأ لا يستهلك شيئًا.
  • على Android تمر عبر معالج الرجوع في الغلاف. بدونها، يؤدي السحب للرجوع مع فتح ورقة إلى إغلاق التطبيق كله.
  • على الويب يضيف عنصر سجل واحدًا طالما سُجّل أي معالج، ويزيله عند إلغاء اشتراك الأخير.
  • ارسم مع ذلك زر رجوع / إغلاق ظاهرًا: كثير من المستخدمين لا يستخدمون إيماءة الرجوع أبدًا.
  • يتولى React Router أصلًا زر الرجوع العادي للصفحة؛ استخدم هذا فقط للحالات التي لا يعرفها الموجّه. ألغِ الاشتراك دائمًا، وإلا ستظل ورقة مغلقة تلتقط ضغطات الرجوع.

appmintNative.onInAppMessage appmintNative#

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

مثال

ينادي كودك لكل رسالة داخل التطبيق (حملات المنشئ) تُعرض أو يُنقر عليها أو تُغلق أو يُجاب عنها على هذا التثبيت. يُرجع دالة إلغاء الاشتراك.

القيم المُعادة: () => void. يحصل رد النداء على {type: 'shown' | 'clicked' | 'dismissed' | 'answered', id, name, template?, presented?, message?, button?, link?, question?, value?}. لا يُطلق أبدًا على الويب. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

export function useInAppAnalytics(log: (event: string, name: string) => void) {
  useEffect(() => onInAppMessage((e) => log('inapp_' + e.type, e.name)), [log]);
}

ارسم الرسائل بنفسك (مع setInAppPresentation('none')، تكون presented هي false ويحمل message التصميم):

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

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

تفاعل مع زر نقر عليه المستخدم في رسم التطبيق نفسه:

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

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

ملاحظات: يكون button هو -1 عند النقر على الرسالة نفسها، وإلا فهو فهرس الزر.

الإجابات (type: 'answered'). الرسالة التي فيها صف نجوم (استبيان النجوم، قيّمنا) أو رقائق درجات من 0 إلى 10 (درجة NPS) تُبلغ بالإجابة مرة واحدة عند إغلاقها: question هي 'stars' (القيمة 1-5) أو 'nps' (القيمة 0-10)، وtemplate يسمّي قالب المعرض الذي جاءت منه. يعرض مركز الإرسال المتوسط أو درجة NPS؛ ويمكن لكودك المتابعة:

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

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

احتفظ بسجلّك الخاص لتقييمات النجوم:

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

onInAppMessage((e) => {
  if (e.type !== 'answered' || e.question !== 'stars') return;
  void fetch('/api/ratings', { method: 'POST', body: JSON.stringify({ stars: e.value, campaign: e.name }) });
});

اشكر من قيّموا بدرجة عالية، في مكوّن React:

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

export function useHappyUser() {
  const [happy, setHappy] = useState(false);
  useEffect(() => onInAppMessage((e) => {
    if (e.type === 'answered' && ((e.question === 'stars' && (e.value ?? 0) >= 4) || (e.question === 'nps' && (e.value ?? 0) >= 9))) setHappy(true);
  }), []);
  return happy;
}

appmintNative.onKeyboard appmintNative#

onKeyboard(cb: (insetPx: number) => void): () => void

مثال

يخبرك بعدد بكسلات الصفحة التي تغطيها لوحة المفاتيح على الشاشة، في كل مرة يتغير ذلك، ليتمكن شريط سفلي من الارتفاع فوقها.

القيمة المُرجعة: دالة لإلغاء الاشتراك - استدعِها عند إزالة المكوّن. تحصل دالة الاستدعاء على رقم بوحدات CSS بكسل: الحشو السفلي الإضافي الذي تحتاجه، 0 عندما تكون لوحة المفاتيح مغلقة. المتطلبات: لا شيء.

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

export function Composer() {
  const [kb, setKb] = useState(0);
  useEffect(() => onKeyboard(setKb), []);          // the returned function unsubscribes

  return (
    <form
      className="fixed inset-x-0 bottom-0 flex gap-2 p-3"
      style={{ paddingBottom: `calc(var(--safe-bottom) + 12px + ${kb}px)` }}
    >
      <input className="flex-1" enterKeyHint="send" aria-label="Message" />
      <button type="submit">Send</button>
    </form>
  );
}

أبقِ قائمة دردشة مُمرَّرة إلى أحدث رسالة أثناء فتح لوحة المفاتيح:

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

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

خطّاف قابل لإعادة الاستخدام، مع فجوة منطقة آمنة تختفي أثناء فتح لوحة المفاتيح:

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

export function useKeyboardInset(): number {
  const [inset, setInset] = useState(0);        // 0 until the keyboard first opens
  useEffect(() => onKeyboard(setInset), []);
  return inset;
}

// In a component: the home-indicator gap is only needed when the keyboard is closed.
// const kb = useKeyboardInset();
// <footer style={{ paddingBottom: kb > 0 ? kb : 'var(--safe-bottom)' }}>...</footer>

ملاحظات:

  • في التطبيق يستمع إلى إضافة Keyboard الخاصة بـ Capacitor ويبلّغ فقط عن جزء لوحة المفاتيح الذي لم يستوعبه التخطيط. يعيد الغلاف عادةً تحجيم الصفحة للوحة المفاتيح، لذلك تكون القيمة غالبًا 0 هناك - وهذا صحيح؛ لا تضف ارتفاع لوحة المفاتيح الخام بنفسك.
  • في تطبيق بملء الشاشة أو بوضع الكشك (لا يُغيَّر حجم الصفحة) يُبلغ عن ارتفاع لوحة المفاتيح بالكامل.
  • على الويب يستخدم visualViewport، ويستدعي الاستدعاء مرة واحدة فورًا، ويعامل ما دون 80 px على أنه واجهة المتصفح (يُبلَّغ عنه كـ 0).
  • في التطبيق يأتي أول استدعاء عند فتح لوحة المفاتيح لأول مرة، لذلك ابدأ حالتك عند 0.

appmintNative.onNetworkChange appmintNative#

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

مثال

يستدعيك كلما انقطع الاتصال أو عاد - لشريط عدم الاتصال، أو لإعادة محاولة الكتابات المصطفة.

القيم المُعادة: دالة إلغاء اشتراك - استدعها عند إزالة المكوّن. المتطلبات: لا شيء.

أعد محاولة العمل المصفوف عند عودة الجهاز إلى الاتصال:

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

export function useRetryWhenOnline(flushQueue: () => Promise<number>) {
  useEffect(() => {
    const off = onNetworkChange(async (s) => {
      if (!s.connected) return;
      const sent = await flushQueue();
      if (sent) toast(`Synced ${sent} change(s)`);
    });
    return off;                                  // unsubscribe on unmount
  }, [flushQueue]);
}

اعرض الحالة الحالية فورًا، ثم تابع التغييرات (لا يُستدعى الاستدعاء عند الاشتراك):

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

export function useOnline(): boolean {
  const [online, setOnline] = useState(() => networkStatus().connected);
  useEffect(() => onNetworkChange((s) => setOnline(s.connected)), []);
  return online;
}

أوقف التنزيلات الكبيرة على بيانات الجوال، وعامل 'none' كحالة بلا اتصال:

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

export function useDataSaver(setHeavyAllowed: (ok: boolean) => void) {
  useEffect(() => onNetworkChange((s) => {
    if (s.type === 'none' || !s.connected) { toast('You are offline'); setHeavyAllowed(false); return; }
    setHeavyAllowed(s.type !== 'cellular');   // 'wifi' and 'unknown' may download freely
  }), [setHeavyAllowed]);
}

ملاحظات:

  • لا يستدعيك بالحالة الحالية عند الاشتراك. اقرأ networkStatus() للحصول على القيمة الابتدائية.
  • في التطبيق يستمع إلى إضافة 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]);
}

حدث خام في صفحة مكتوبة يدويًا (تطبيق Android):

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

انشر إشعارًا بأزرار فقط حيث يدعمها الإنشاء، وتعامل مع النقرة بحسب وسم الإشعار:

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

export async function remindWithButtons(taskId: string, title: string) {
  if (can('notificationActions') === 'none') { toast(title); return; }   // no buttons here
  await notifyWithActions({
    title, body: 'Tap a button', tag: `task-${taskId}`,
    actions: [{ id: 'done', title: 'Done' }, { id: 'later', title: 'Later' }],
  });
}

export function useTaskActions(complete: (id: string) => void, snooze: (id: string) => void) {
  useEffect(() => onNotificationAction(({ actionId, tag }) => {
    const id = tag.replace(/^task-/, '');
    if (actionId === 'done') complete(id);
    else if (actionId === 'later') snooze(id);
  }), [complete, snooze]);
}

ملاحظات: في تطبيق Android هذا هو الحدث appmint:notification-action (وأيضًا window.onAppMintNotificationAction(detail))؛ وعلى الويب لا يفعل شيئًا. تُسلَّم النقرة فقط أثناء تشغيل التطبيق - نقر زر لا يفتح التطبيق.

appmintNative.onNotificationClick appmintNative#

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

مثال

يشغّل دالة استدعاء عندما ينقر المستخدم على إشعار نشره التطبيق - بما في ذلك النقرة التي شغّلت التطبيق من البداية - ليتمكن التطبيق من فتح الشاشة الصحيحة.

القيمة المُعادة: دالة إلغاء الاشتراك. يتلقى الاستدعاء { tag, title, body, data } - data هو ما مرّرته إلى notify({ …, data }). المتطلبات: الإشعارات، مثل notify(). الحدث وراءها: appmint:notification-click.

import { useEffect } from 'react';
import { notify, onNotificationClick } from '@/lib/appmintNative';

export function useOrderAlerts(navigate: (path: string) => void) {
  useEffect(() => onNotificationClick((e) => {
    const d = e.data as { orderId?: number } | null;
    if (d?.orderId) navigate(`/orders/${d.orderId}`);
  }), [navigate]);
}

async function orderShipped(orderId: number) {
  await notify({ title: 'Order shipped', body: `#${orderId} is on its way`, tag: `order-${orderId}`, data: { orderId } });
}

افتح الشاشة الصحيحة بعد النقرة، حتى لو بدأت التطبيق من إيقاف كامل، وتجاهل الإشعارات التي لا تحمل بيانات:

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

export function useNotificationRouting(navigate: (path: string) => void) {
  useEffect(() => onNotificationClick(({ tag, data }) => {
    const d = data as { screen?: string; id?: string } | null;
    if (!d?.screen) { console.log('plain notification tapped:', tag); return; }
    navigate(d.id ? `/${d.screen}/${d.id}` : `/${d.screen}`);
  }), [navigate]);
}

أرسل إشعارًا فقط حيث توجد إشعارات، مع بيانات للنقرة (تحتاج data إلى tag):

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

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

ملاحظات: تنتقل النقرة عبر شاشة البداية وشاشة PIN إلى الصفحة. يجب أن تكون data بصيغة JSON (أقل من 16 كيلوبايت) وتحتاج إلى tag.

appmintNative.onPullToRefresh appmintNative#

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

مثال

يضيف إيماءة السحب للأسفل للتحديث إلى قائمة قابلة للتمرير: اسحب للأسفل في الأعلى، ثم أفلت، فتُعاد تحميل بياناتك.

القيم المُعادة: دالة إلغاء اشتراك - استدعها عند إزالة المكوّن. المتطلبات: لا شيء.

el هو العنصر الذي يتم تمريره (أو document.documentElement عندما تُمرَّر الصفحة كلها). يبقى المؤشر الدوّار حتى يتم الوعد القادم من onRefresh:

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

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

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

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

صفحة تتمرر ككل، بسحب أطول:

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

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

حدّث قائمة عبر خطّاف بيانات، مع نقرة اهتزازية عند وصول البيانات:

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

export function useListRefresh(refetch: () => Promise<unknown>) {
  const ref = useRef<HTMLUListElement>(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    return onPullToRefresh(el, async () => {
      await refetch();
      haptic('success');                     // the spinner stays until this promise settles
    }, { threshold: 80 });
  }, [refetch]);
  return ref;                                // <ul ref={ref} className="h-full overflow-y-auto">
}

ملاحظات:

  • يبدأ فقط عندما تكون القائمة ممررة إلى الأعلى تمامًا. الافتراضي threshold هو 72 px من السحب.
  • يرسم مؤشرًا دائريًا بألوان التطبيق (--surface و--accent)، ويطلق اهتزازًا متوسطًا عند نقطة الإتمام، ويتجاهل السحبات الجديدة أثناء تشغيل التحديث.
  • يحدّث بياناتك لا الصفحة. سحب إعادة تحميل الصفحة الخاص بالغلاف (تفعيل السحب للتحديث في الخطوة 3 (التكامل)) متوقف افتراضيًا - أبقِه متوقفًا عند استخدام هذا، كي لا يعيد السحب تحميل الصفحة أيضًا.
  • مبني على Pointer Events، لذا هو نفسه على كل واجهة. يضبط overscroll-behavior-y: contain على el ويعيده عند إلغاء الاشتراك.

appmintNative.onShared appmintNative#

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

مثال

يستدعيك عندما يشارك تطبيق آخر نصًا أو رابطًا إلى هذا التطبيق وهو مفتوح أصلًا.

القيمة المُعادة: دالة لإلغاء الاشتراك - استدعِها عند الإزالة. المتطلبات: خيار Receive Shares From Other Apps في النسخة المبنية مع "Text and links" - يُفعَّل تلقائيًا عندما يستدعي كودك 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)} />;
}

ملاحظات:

  • Android: يستمع إلى حدث appmint:shared من الغلاف؛ ويستدعي الغلاف أيضًا window.onAppMintShared(detail) عندما تعرّفه الصفحة. وتُحفظ الحمولة نفسها أيضًا من أجل sharedText() حتى clearShared().
  • على الويب لا يُطلق أبدًا ويُعيد دالة إلغاء اشتراك لا تفعل شيئًا.

appmintNative.onVolumeChange appmintNative#

onVolumeChange(cb) — @/lib/appmintNative

مثال

يستدعي cb كلما تغيّر أحد مستويات صوت الهاتف - مفاتيح الجهاز أو شريط النظام أو تطبيق آخر.

القيمة المُعادة: دالة توقف الاستماع (آخر من يتوقف يُطفئ المراقبة الأصلية). يتلقى الاستدعاء { stream, current, max, percent, muted? } لكل مجرى تغيّر. لا يفعل شيئًا على الويب. المتطلبات: لا شيء.

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

export function MediaVolumeBadge() {
  const [pct, setPct] = useState(() => getVolume('media')?.percent ?? null);
  useEffect(() => onVolumeChange((v) => { if (v.stream === 'media') setPct(v.percent); }), []);
  return pct === null ? null : <span>🔊 {pct}%</span>;
}

حذّر عند خفض مستوى صوت الوسائط إلى الصفر أثناء تشغيل فيديو، وأوقف الاستماع عند الإزالة:

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

export function useQuietWarning(playing: boolean) {
  useEffect(() => {
    if (!playing) return;
    return onVolumeChange((v) => {
      if (v.stream === 'media' && (v.percent === 0 || v.muted)) toast('Volume is off');
    });
  }, [playing]);
}

اعكس مستوى الهاتف على منزلق واكتب تغيير المستخدم إلى الهاتف (لا يوجد صوت على الويب، لذا getVolume هي null هناك):

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

export function MediaVolumeSlider() {
  const [pct, setPct] = useState<number | null>(() => getVolume('media')?.percent ?? null);
  useEffect(() => onVolumeChange((v) => { if (v.stream === 'media') setPct(v.percent); }), []);
  if (pct === null) return null;                       // not the Android app
  return (
    <input type="range" min={0} max={100} value={pct}
      onChange={(e) => { const r = setVolume('media', Number(e.target.value)); if (r.ok) setPct(Number(e.target.value)); }} />
  );
}

appmintNative.openNotificationSettings appmintNative#

openNotificationSettings(): Promise<boolean>

مثال

يفتح شاشة إعدادات الإشعارات الخاصة بهذا التطبيق (تطبيق Android).

القيمة المُعادة: Promise<boolean> - صحيحة عندما فُتحت الشاشة؛ وخاطئة على الويب وفي أي مكان لا توجد فيه هذه الشاشة. تحقق من 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 التي ينشئها خادم مفاتيح المرور لديك. في تطبيق Android يستخدم مدير مفاتيح المرور في Android؛ وعلى الويب يستخدم navigator.credentials.

القيمة المُرجعة: available() ← 'native' | 'web' | 'none'؛ create(optionsJSON) ← وعد بـ RegistrationResponseJSON؛ get(optionsJSON) ← وعد بـ AuthenticationResponseJSON (أرسل أيًّا منهما بـ POST إلى خادمك). وتُرفض بـ DOMException (NotAllowedError، InvalidStateError، SecurityError، NotSupportedError). origin() ← نص، assetLinks() ← مصفوفة (في التطبيق فقط؛ '' / [] على الويب). المتطلبات: خادم مفاتيح مرور؛ وفي التطبيق أيضًا أندرويد 9+ مع خدمات Google Play ومجالك assetlinks.json.

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

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

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

قيم الإعداد لمرة واحدة، من التطبيق المثبّت عبر Google Play:

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

console.log(JSON.stringify(passkeys.assetLinks(), null, 2));  // → /.well-known/assetlinks.json
console.log(passkeys.origin());                               // → add to expected origins

اعرض زر مفتاح المرور فقط حيث يمكن أن يعمل، وقل ذلك عندما لا يمكن:

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 على نطاقك. الملء التلقائي لمفاتيح المرور (mediation: 'conditional') غير متاح في التطبيق - ابدأ من زر.

appmintNative.paste appmintNative#

paste(): Promise<string>

مثال

يقرأ النص الموجود في حافظة النظام.

القيم المُعادة: Promise<string> - نص الحافظة، أو '' عندما تكون الحافظة فارغة. يُرفض عندما لا يستطيع هذا المتصفح قراءة الحافظة إطلاقًا، أو عندما يرفض المستخدم الإذن. المتطلبات: لا شيء.

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

export function PasteCode({ onCode }: { onCode: (c: string) => void }) {
  if (can('clipboardRead') === 'none') return null;   // e.g. Firefox and Safari
  return (
    <button
      onClick={async () => {
        try {
          const text = (await paste()).trim();
          if (!text) { toast('The clipboard is empty'); return; }
          onCode(text);
        } catch {
          toast('Clipboard access was blocked');       // denied permission, not "empty"
        }
      }}
    >
      Paste code
    </button>
  );
}

الصيغة بنمط Capacitor:

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

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

الصق في مربع نص برسالة لكل نتيجة، واقرنه مع copy:

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

export async function pasteInto(setValue: (s: string) => void) {
  try {
    const text = await paste();
    if (text) setValue(text);
    else toast('Nothing to paste');
  } catch {
    toast('Allow clipboard access to paste');
  }
}

export async function shoutClipboard() {
  const text = await paste().catch(() => '');
  if (text && await copy(text.toUpperCase())) toast('Converted and copied');
}

ملاحظات: في التطبيق تقرأ إضافة Clipboard النص؛ والحافظة الفارغة تعطي ''، وليس خطأ. في المتصفح هي navigator.clipboard.readText() (Chromium فقط)، وتطلب إذن المستخدم - استدعِها من نقرة.

appmintNative.pickContact appmintNative#

pickContact(): Promise<Contact | null>

مثال

يفتح منتقي جهات اتصال النظام ليختار المستخدم شخصًا واحدًا: منتقي Android في التطبيق، وContact Picker API في Chrome على Android.

القيمة المُرجعة: Promise<Contact | null> - null عندما يلغي المستخدم أو يُرفض الوصول. المتطلبات: لا شيء (منتقي النظام لا يحتاج إذنًا).

اختر شخصًا واملأ نموذجًا. لجهة الاتصال الشكل نفسه في التطبيق وعلى الويب: { id?, name?, phones?: string[], emails?: string[], photoUri? }.

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

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

زر "اختر جهة اتصال" يعرض الاختيار، وحقل رقم بسيط حيث لا يوجد المنتقي:

import { useState } from 'react';
import { can, pickContact, type Contact } from '@/lib/appmintNative';

export function InvitePicker() {
  const [who, setWho] = useState<Contact | null>(null);
  if (can('contactPick') === 'none') return <input placeholder="Phone number" />;   // type it by hand
  return (
    <div>
      <button onClick={async () => setWho((await pickContact()) ?? who)}>Choose a contact</button>
      {who && <p>{who.name} {who.phones?.[0]}</p>}
    </div>
  );
}

سلّم الرقم المختار إلى تطبيق الرسائل، مع رسالة عندما لا يملك الشخص رقمًا:

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

export async function textAFriend(message: string) {
  const c = await pickContact();
  if (!c) return;                              // cancelled
  const phone = c.phones?.[0];
  if (!phone) { toast(`${c.name ?? 'That contact'} has no phone number`); return; }
  window.location.href = `sms:${encodeURIComponent(phone)}?body=${encodeURIComponent(message)}`;
}

ملاحظات: في التطبيق يعود الرقم الذي نُقر عليه فقط، وليس البريد الإلكتروني أبدًا، وphotoUri عنوان content:// لصورة جهة الاتصال. can('contactPick') هي 'none' في المتصفحات التي لا تملك Contact Picker API (سطح المكتب، Safari) - لا ترسم الزر هناك.

appmintNative.pickFile appmintNative#

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

مثال

يتيح للمستخدم اختيار ملف واحد وقراءته كنص. في التطبيق يفتح منتقي المستندات في Android؛ وعلى الويب يفتح حقل ملف عاديًا.

القيمة المُرجعة: Promise<PickedFile | null> - { name, mime, size, text(): Promise<string> }، أو null فقط عندما يلغي المستخدم. تُرفض بـ Error يكون code فيه 'no-picker' (الهاتف ليس فيه تطبيق منتقي مستندات)، أو 'disabled' أو 'failed' عندما لم يمكن فتح أي منتقٍ. تقرأ text() الملف كاملًا وتُرفض برمز الجسر (مثل 'not-found') عندما تتعذّر. المتطلبات: لا شيء. مع تحديد الوصول الأصلي إلى المجلدات (SAF) في الخطوة 4 (الوصول) يستخدم التطبيق منتقي مستندات أندرويد؛ وبدونه يستخدم التطبيق منتقي الملفات الخاص بالصفحة.

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

async function importCsv() {
  const f = await pickFile('text/csv');          // default '*/*' = any file
  if (!f) return;                                // cancelled (or picker not enabled in the build)
  if (f.size > 2_000_000) { alert('Please choose a file under 2 MB.'); return; }
  try {
    setRows(parseCsv(await f.text()));
  } catch (e) {
    alert(`Could not read ${f.name}: ${String(e)}`);
  }
}

استورد نسخة احتياطية JSON وتعامل مع كل طريقة قد يفشل بها pickFile:

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

export async function restoreBackup(apply: (data: unknown) => void) {
  try {
    const f = await pickFile('application/json');
    if (!f) return;                                    // cancelled
    apply(JSON.parse(await f.text()));
    toast(`Restored ${f.name}`);
  } catch (e) {
    const code = (e as { code?: string }).code;
    if (code === 'no-picker') toast('This phone has no file picker app');
    else if (e instanceof SyntaxError) toast('That file is not a valid backup');
    else toast('Could not open the file');
  }
}

اختر ملفًا نصيًا وضعه في محرر، بجانب تفاصيل الملف:

import { useState } from 'react';
import { pickFile } from '@/lib/appmintNative';

export function OpenText() {
  const [doc, setDoc] = useState<{ name: string; kb: number; text: string } | null>(null);
  return (
    <div>
      <button onClick={async () => {
        const f = await pickFile('text/*');
        if (f) setDoc({ name: f.name, kb: Math.ceil(f.size / 1024), text: await f.text() });
      }}>Open a text file</button>
      {doc && <><h3>{doc.name} ({doc.kb} KB)</h3><textarea defaultValue={doc.text} /></>}
    </div>
  );
}

ملاحظات: في التطبيق يقرأ text() الملف على قطع من 4 ميغابايت حتى النهاية ويفك ترميزه بـ UTF-8، فتُقرأ الملفات الكبيرة كاملة - ضع حدك الخاص للحجم الذي تتحمله الشاشة. المنتقي الذي لا يستطيع الفتح يُرفض، لذا غلّف pickFile() أيضًا بـ try/catch. على الويب يكون text() هو File.text() الخاص بالمتصفح. للملفات الثنائية أو الكتابة مرة أخرى تستخدم الصفحات المكتوبة يدويًا WebToApkFS.

appmintNative.pip appmintNative#

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

مثال

يُبقي فيديو يعمل في نافذة عائمة صغيرة بينما يفعل المستخدم شيئًا آخر.

القيمة المُرجعة: enter() تُرجع Promise<boolean> - true عند فتح النافذة العائمة. exit() تُرجع Promise<void>. لا تُرفض أيٌّ منهما. المتطلبات: في تطبيق أندرويد خيار صورة داخل صورة (PiP) في الإنشاء ضمن الخطوة 3 (التكامل) - ويُفعَّل تلقائيًا عندما يستدعي كودك pip.enter(.

زر "تصغير" في مشغّل:

import { useRef } from 'react';
import { can, pip, toast } from '@/lib/appmintNative';

export function Player({ src }: { src: string }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  return (
    <div>
      <video ref={videoRef} src={src} controls playsInline />
      {can('pip') !== 'none' && (
        <button
          onClick={async () => {
            const ok = await pip.enter(videoRef.current ?? undefined);
            if (!ok) toast('Picture-in-picture is not available here');
          }}
        >
          Minimise
        </button>
      )}
    </div>
  );
}

اتركه عند إغلاق المشغّل (الويب):

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

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

ادخل في وضع الصورة داخل الصورة تلقائيًا عندما يغادر المستخدم فيديو قيد التشغيل، فقط حيث يكون مدعومًا:

import { useEffect, type RefObject } from 'react';
import { can, pip } from '@/lib/appmintNative';

export function useAutoPip(videoRef: RefObject<HTMLVideoElement>) {
  useEffect(() => {
    if (can('pip') === 'none') return;
    const onHide = () => {
      const v = videoRef.current;
      if (document.hidden && v && !v.paused) void pip.enter(v);
    };
    document.addEventListener('visibilitychange', onHide);
    return () => document.removeEventListener('visibilitychange', onHide);
  }, [videoRef]);
}

ملاحظات:

  • أندرويد (8+): يتقلص التطبيق بأكمله إلى النافذة العائمة، ويُتجاهل المعامل video. يُرجع false على أندرويد الأقدم أو في إنشاء بدون خيار PiP. يوسّع المستخدم النافذة للعودة؛ وexit() لا تفعل شيئًا هناك.
  • الويب: صورة داخل صورة الخاصة بعنصر <video> نفسه. يجب أن يكون الفيديو محمّلًا (يعمل عادةً)؛ والاستدعاء بدون video يُرجع false.
  • استدعِها من نقرة.

appmintNative.platform appmintNative#

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

مثال

يخبرك على أي واجهة يعمل الكود: تطبيق Android المثبّت، أو متصفح ويب.

القيم المُعادة: سلسلة نصية فورًا: 'android' داخل التطبيق، 'web' في المتصفح. يسأل Capacitor أولًا (Capacitor.getPlatform())، ثم يفحص جسر غلاف Android، وإلا 'web'. يسرد النوع أيضًا 'ios'، لكن تطبيق AppMint لا يحصل عليه أبدًا - فـ AppMint ينشئ تطبيقات Android فقط. المتطلبات: لا شيء.

اختر الرابط المناسب وانسخه.

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

const inApp = platform() === 'android';

const rateLink = inApp
  ? 'https://play.google.com/store/apps/details?id=com.example.app'
  : '/feedback';

const backHint = inApp ? 'Press Back to go back' : 'Use the browser\'s back button';

اعرض عناصر تحكم خاصة بالتطبيق فقط.

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

export function InstallBanner() {
  // In the installed app there is nothing to install.
  if (platform() === 'android') return null;
  return <a href="https://play.google.com/store/apps/details?id=com.example.app">Get the Android app</a>;
}

ضع وسومًا على أحداث التحليلات بالواجهة.

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

export function track(event: string, data: Record<string, unknown> = {}) {
  return fetch('/api/track', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ event, surface: platform(), ...data }),
  });
}

ملاحظات: لا تُجيب platform() أبدًا بـ 'web' داخل التطبيق المثبّت. لا تستخدمها لتقرير ما إذا كانت ميزة موجودة - فكثير من الميزات تعتمد أيضًا على مفاتيح الإنشاء؛ وcan('<capability>') تجيب عن ذلك.

appmintNative.privacyScreen appmintNative#

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

مثال

يمنع لقطات الشاشة وتسجيل الشاشة ومعاينة مبدّل التطبيقات أثناء فتح شاشة فيها أسرار.

القيم المُعادة: enable() ← Promise<boolean> (false حيث لا شيء يمكنه المنع)؛ disable() ← Promise<void>؛ isEnabled() ← Promise<boolean>. المتطلبات: لا شيء. في تطبيق Android يستخدم حظر لقطات الشاشة في الغلاف (لقطات الشاشة وتسجيل الشاشة والبث والصورة المصغرة للتطبيقات الأخيرة).

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

/** Use in the component that shows the secret (a card number, a statement). */
export function usePrivacyScreen() {
  useEffect(() => {
    void privacyScreen.enable();
    return () => { void privacyScreen.disable(); };   // e.g. a modal closed without a route change
  }, []);
}

أخبر المستخدم بما هو محمي - فقط عندما يكون كذلك فعلًا:

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

export async function protectionLabel(): Promise<string> {
  if (can('privacyScreen') === 'none') return '';           // web: nothing can stop a screenshot
  return (await privacyScreen.isEnabled()) ? 'Screenshots are blocked on this screen' : '';
}

شغّل الحماية لعرض حساس واحد وأخبر المستخدم إذا تعذّر تطبيقها:

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

export async function openStatement(show: () => void) {
  const protectedNow = await privacyScreen.enable();   // false where nothing can block capture
  if (!protectedNow) toast('Screenshots cannot be blocked here');
  show();
}

export async function closeStatement(hide: () => void) {
  hide();
  await privacyScreen.disable();
}

ملاحظات: في التطبيق ينتمي الحظر للصفحة الحالية ويُحرَّر عند انتقال التطبيق إلى مسار آخر، فشغّله مجددًا عند ظهور تلك الشاشة. على الويب تكون can('privacyScreen') هي 'none' ويُرجع enable() القيمة false - فلا تدّعِ الحماية هناك. لا شيء يمنع كاميرا موجّهة إلى الشاشة.

appmintNative.pushInbox appmintNative#

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

مثال

يعيد رسائل AppMint Push التي استلمها هذا التثبيت (الأحدث أولًا، حتى 50)، لشاشة إشعارات داخل التطبيق.

القيمة المُعادة: PushMessage[] بشكل متزامن: { id, title, body, imageUrl, link, receivedAt, read }. مصفوفة فارغة على الويب وعند إيقاف AppMint Push. المتطلبات: Android: AppMint Push مفعّل في خطوة Integrate (الخطوة 3) عند الإنشاء.

import { useEffect, useState } from 'react';
import { pushInbox, markPushRead, type PushMessage } from '@/lib/appmintNative';

export function useInbox() {
  const [items, setItems] = useState<PushMessage[]>(() => pushInbox());
  useEffect(() => {
    const refresh = () => setItems(pushInbox());
    // detail: { type: 'received', message } or { type: 'revoked', id }
    window.addEventListener('appmint:push', refresh);
    return () => window.removeEventListener('appmint:push', refresh);
  }, []);
  const unread = items.filter((m) => !m.read).length;
  const open = (m: PushMessage) => {
    markPushRead(m.id);
    setItems(pushInbox());
  };
  return { items, unread, open };
}

اعرض عدد الشارات غير المقروءة بحيث يتبع الوارد الجديد:

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

export function UnreadBadge() {
  const [n, setN] = useState(() => pushInbox().filter((m) => !m.read).length);
  useEffect(() => {
    const recount = () => setN(pushInbox().filter((m) => !m.read).length);
    window.addEventListener('appmint:push', recount);
    return () => window.removeEventListener('appmint:push', recount);
  }, []);
  return n > 0 ? <span aria-label={`${n} unread`}>{n}</span> : null;
}

اعرض القائمة، مع حالة فارغة للويب وللنسخ بدون AppMint Push، وزر "تحديد الكل كمقروء":

import { useState } from 'react';
import { pushInbox, markPushRead } from '@/lib/appmintNative';

export function NotificationsScreen() {
  const [items, setItems] = useState(() => pushInbox());
  if (items.length === 0) return <p>No notifications yet.</p>;
  return (
    <section>
      <button onClick={() => { markPushRead(); setItems(pushInbox()); }}>Mark all read</button>
      <ul>
        {items.map((m) => (
          <li key={m.id} style={{ fontWeight: m.read ? 400 : 700 }}>
            {m.title} <small>{new Date(m.receivedAt).toLocaleString()}</small>
            <p>{m.body}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

ملاحظات: الصندوق الوارد هو الحقيقة المحفوظة - اقرأه عند التحميل؛ ولا يُطلق الحدث appmint:push إلا أثناء ظهور التطبيق على الشاشة. الرسائل المُغلقة مستثناة.

appmintNative.readBarcodeFrom appmintNative#

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

مثال

يقرأ الباركود ورموز QR من صورة تملكها الصفحة أصلًا - ملف صورة أو Blob أو <img> أو <canvas> أو إطار فيديو. لا تُفتح أي كاميرا.

القيمة المُعادة: Promise<{ value: string; format: string }[]> - كل رمز تم العثور عليه، بترتيب الكاشف؛ ومصفوفة فارغة عند عدم وجود شيء، أو عند تعذر قراءة الصورة، أو عند عدم توفر قراءة الباركود. لا يُرفض أبدًا. المتطلبات: لا شيء.

اقرأ رمزًا من صورة يختارها المستخدم:

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

export function ReadFromPhoto({ onCode }: { onCode: (v: string) => void }) {
  if (can('barcodeScan') === 'none') return null;
  return (
    <label>
      Read a code from a photo
      <input
        type="file"
        accept="image/*"
        onChange={async (e) => {
          const file = e.currentTarget.files?.[0];
          if (!file) return;
          const codes = await readBarcodeFrom(file, ['qr_code']);
          if (codes.length) onCode(codes[0].value);
          else toast('No QR code found in that photo');
        }}
      />
    </label>
  );
}

من صورة موجودة أصلًا في الصفحة:

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

const img = document.querySelector('img#ticket') as HTMLImageElement;
const [first] = await readBarcodeFrom(img);
if (first) console.log(first.format, first.value);

اقرأ رمزًا من إطار canvas، مع التحقق مما يدعمه الجهاز أولًا:

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

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

ملاحظات:

  • يستخدم BarcodeDetector الخاص بمحرك المتصفح (موجود في WebView لتطبيق Android؛ غير موجود في Safari وكثير من متصفحات سطح المكتب - وعندها يُرجع []).
  • يفتح <input type="file" accept="image/*"> العادي منتقي صور Android ولا يحتاج إذن تخزين.
  • حدّد formats عندما تعرفها: فهو أسرع ويخطئ أقل. يعرض barcodeFormats() ما يدعمه الجهاز.

appmintNative.readOtp appmintNative#

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

مثال

ينتظر رمز SMS لمرة واحدة لهذا التطبيق ويعيده - ورقة موافقة SMS من Google في تطبيق Android، وWebOTP في Chrome على الويب.

القيم المُعادة: Promise<string | null> - الرمز، أو null عندما رفض المستخدم أو مرّت خمس دقائق أو ألغى signal. يُلقي استثناءً عندما تكون can('otp') هي 'none'. المتطلبات: لا شيء لتشغيله. يجب أن تنتهي الرسالة بـ @<host> #<code> (في تطبيق Android يكون المضيف appassets.androidplatform.net).

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

export function CodeField({ onDone }: { onDone: (code: string) => void }) {
  const [code, setCode] = useState('');

  useEffect(() => {
    if (can('otp') === 'none') return;
    const ac = new AbortController();
    readOtp({ signal: ac.signal }).then((c) => { if (c) { setCode(c); onDone(c); } });
    return () => ac.abort();          // leaving the screen stops the wait
  }, [onDone]);

  return (
    <input value={code} onChange={(e) => setCode(e.target.value)}
      inputMode="numeric" autoComplete="one-time-code" aria-label="Verification code" />
  );
}

عالج إجابة "لا رمز"، واعرض إعادة إرسال عندما ينتهي الانتظار دون رمز:

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

export function VerifyStep({ resend, submit }: { resend: () => void; submit: (c: string) => void }) {
  const [waited, setWaited] = useState(false);

  async function start() {
    resend();
    if (can('otp') === 'none') return;          // readOtp would throw here — the user types it
    const code = await readOtp();               // null: declined, 5 minutes passed, or aborted
    if (code) submit(code); else setWaited(true);
  }

  return (
    <div>
      <button onClick={start}>Send code</button>
      {waited && <p>No code detected. <button onClick={start}>Send again</button></p>}
    </div>
  );
}

التقط الاستثناء عندما لا تستطيع فحص can('otp') أولًا:

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

export async function tryAutoFill(): Promise<string | null> {
  try {
    return await readOtp({ signal: AbortSignal.timeout(60_000) });
  } catch {
    return null;                                // one-time codes unavailable: the user types it
  }
}

ملاحظات: هذا وسيلة تسريع - أبقِ الحقل قابلًا للكتابة دائمًا.

appmintNative.recognizeText appmintNative#

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

مثال

يقرأ النص في صورة على الجهاز (can('ocr')): قارئ ML Kit في تطبيق Android، أو TextDetector في Chrome حيث يكون مفعّلًا.

القيمة المُرجعة: وعد بـ { text, blocks: [{ text, box, lines: [{ text, box }] }] }، مع مربعات بوحدات بكسل الصورة. يُرفض بـ code 'unavailable' حيث تكون can('ocr') هي 'none'، أو DOMException عند فشل التعرّف. المتطلبات: لا شيء، ولا إذن.

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

export function ReceiptReader() {
  const [total, setTotal] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  if (can('ocr') === 'none') return null;
  return (
    <label>
      Scan a receipt
      <input type="file" accept="image/*" capture="environment" onChange={async (e) => {
        const file = e.target.files?.[0];
        if (!file) return;
        setBusy(true);
        try {
          const { text } = await recognizeText(file);
          setTotal(/total\s*[:$€£]?\s*([\d.,]+)/i.exec(text)?.[1] ?? 'not found');
        } finally { setBusy(false); }
      }} />
      {busy ? <span role="status">Reading…</span> : total && <strong>Total: {total}</strong>}
    </label>
  );
}

عالج حالتي "لا قارئ" والفشل صراحةً:

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

export async function readLabel(file: Blob): Promise<string> {
  if (can('ocr') === 'none') return '';
  try {
    const { text } = await recognizeText(file);
    return text.trim();
  } catch (e) {
    if ((e as { code?: string }).code === 'unavailable') return '';
    throw e;                                    // a real recognition failure
  }
}

اقرأ نصًا بغير الأبجدية اللاتينية، واعرض كل سطر مع موضعه:

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

export async function readHindiSign(img: HTMLImageElement) {
  const { blocks } = await recognizeText(img, { script: 'devanagari' });
  return blocks.flatMap((b) => b.lines).map((l) => ({ text: l.text, box: l.box }));
}

ملاحظات: الكتابات اللاتينية فقط. قد يستغرق الاستدعاء الأول على هاتف جديد بضع ثوانٍ بينما تجلب خدمات Play النموذج - اعرض التقدم.

appmintNative.remoteConfig appmintNative#

remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()

مثال

يرسم شريط الإعلان وشاشة الصيانة اللذين ينشرهما المنشئ من Remote Update - للصفحة عندما تعمل في متصفح ويب. في تطبيق Android يقوم الغلاف بذلك بنفسه، لذلك لا تفعل هذه الاستدعاءات شيئًا هناك.

القيم المُعادة: start(manifestUrl?) ← Promise<void> (يجلب ويرسم؛ لا يُرفض أبدًا)؛ fetch(url) ← Promise<RemoteConfig | null> (لا يُرفض أبدًا؛ null = لا شيء ينطبق)؛ apply(cfg) ← دالة إزالة؛ manifestUrl() ← string (قيمة content الخاصة بـ <meta name="appmint-remote-manifest">، و'' عند عدم وجودها)؛ handledHere() ← boolean؛ dismissed(id) ← boolean؛ dismiss(id) ← void. المتطلبات: لا شيء داخل التطبيق - فالتحديث عن بُعد مدمج فيه أصلًا.

آمن للاستدعاء عند البدء؛ وفي التطبيق يعود فورًا:

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 في تطبيق أندرويد: يقرأ الغلاف ملف manifest أصليًا ويرسم الإعلان بنفسه، فلا ترى لافتتين أبدًا.
  • start() بدون وسيط يستخدم manifestUrl(). لا يضيف إنشاء AppMint وسم meta هذا، لذا بدون وسيط (أو <meta name="appmint-remote-manifest" content="…"> الخاص بك) لا يُجلب شيء.
  • fetch() تُرجع null عند نشر متوقف أو مُتراجع عنه، أو ملف manifest لا تتضمن قائمة platforms فيه web، أو تنسيق manifest أحدث، أو أي خطأ في الشبكة.
  • وضع الصيانة هو الأعلى: يغطي 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 مفعّل في خطوة Integrate (الخطوة 3) عند الإنشاء.

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

let lastDismissed: string | null = null;

export function dismiss(id: string) {
  if (dismissPushMessage(id)) lastDismissed = id;
  return pushInbox();
}

export function undo() {
  if (lastDismissed) restorePushMessage(lastDismissed);
  lastDismissed = null;
  return pushInbox();
}

اعرض خيار تراجع (Undo) بعد السحب للإزالة مباشرة، في صف React صغير:

import { useState } from 'react';
import { dismissPushMessage, restorePushMessage } from '@/lib/appmintNative';

export function SwipeRow({ id, onChange }: { id: string; onChange: () => void }) {
  const [undoable, setUndoable] = useState(false);
  return undoable ? (
    <button onClick={() => { if (restorePushMessage(id)) { setUndoable(false); onChange(); } }}>
      Undo
    </button>
  ) : (
    <button onClick={() => { if (dismissPushMessage(id)) { setUndoable(true); onChange(); } }}>
      Dismiss
    </button>
  );
}

تحقق من الإجابة، لأن معرّفًا غير معروف أو لم يُغلَق أبدًا يعطي false:

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

export function undoOrReport(id: string): string {
  if (!restorePushMessage(id)) return 'Nothing to restore';
  return `Back in your inbox (${pushInbox().length} messages)`;
}

ملاحظات: الرسائل المحذوفة بـ clearPushInbox() أو المسحوبة منك لا يمكن استعادتها.

appmintNative.safeArea appmintNative#

safeArea(): { top: number; right: number; bottom: number; left: number }

مثال

يعطي المساحة التي يشغلها شريط الحالة وفتحة الكاميرا وشريط التنقل، بوحدات CSS بكسل، حتى لا تقع الأشرطة الثابتة تحتها.

القيمة المُرجعة: أرقام { top, right, bottom, left }، بشكل متزامن. المتطلبات: لا شيء.

في معظم الأحيان يكفي CSS - يضع الوقت التشغيلي --safe-top و--safe-right و--safe-bottom و--safe-left على :root:

export function BottomNav() {
  return (
    <nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
      {/* tabs */}
    </nav>
  );
}

استخدم الأرقام عندما يجب أن يجري الكود الحسابات، مثلًا لوضع زر عائم:

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

export function useBottomInset(): number {
  const [bottom, setBottom] = useState(0);
  useEffect(() => {
    const read = () => setBottom(safeArea().bottom);
    read();
    window.addEventListener('resize', read);        // rotation changes the insets
    return () => window.removeEventListener('resize', read);
  }, []);
  return bottom;
}

أبقِ قائمة قابلة للتمرير بعيدة عن الأشرطة بضبط حشوها من الأرقام:

import { useEffect, useState, type ReactNode } from 'react';
import { safeArea } from '@/lib/appmintNative';

export function PaddedList({ children }: { children: ReactNode }) {
  const [inset, setInset] = useState({ top: 0, bottom: 0 });
  useEffect(() => {
    const read = () => { const s = safeArea(); setInset({ top: s.top, bottom: s.bottom }); };
    read();
    window.addEventListener('resize', read);
    return () => window.removeEventListener('resize', read);
  }, []);
  return <div style={{ paddingTop: inset.top + 8, paddingBottom: inset.bottom + 8 }}>{children}</div>;
}

ملاحظات:

  • يُقاس من CSS env(safe-area-inset-*). تُخزَّن القيمة مؤقتًا وتُقاس من جديد بعد تغيير حجم النافذة أو تدويرها.
  • قبل وجود document.body (مبكرًا جدًا وقت الاستيراد) يُرجع أصفارًا كلها - استدعه من 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);

احفظ نتيجة fetch، وأخبر المستخدم عندما يتعذّر بدؤها:

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

export async function downloadInvoice(id: string) {
  const res = await fetch(`/api/invoices/${id}.pdf`);
  if (!res.ok) { toast('Could not get the invoice'); return; }
  const pdf = await res.blob();                          // the Blob keeps its own type
  if (!saveFile(`Invoice-${id}.pdf`, pdf)) toast('Could not start the download');
}

ملاحظات: هو <a download> عادي في الخلفية، يحوّله التطبيق إلى ورقة "حفظ باسم…" (انظر downloads). مرّر دائمًا اسمًا بامتداد؛ وبدونه يُحفظ الملف باسم download. نوع MIME الافتراضي للنص هو text/plain.

appmintNative.saveToGallery appmintNative#

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

مثال

يحفظ صورة أو فيديو في معرض الهاتف - قيمة can('gallery') هي 'native' في تطبيق Android و'web' في المتصفح (يُنزَّل الملف هناك).

القيمة المُرجعة: وعد بـ URI العنصر المحفوظ ('' على الويب). يُرفض بـ code 'permission-denied' أو 'download-failed' أو 'bad-source' أو 'failed'. المتطلبات: لا شيء؛ ولا إذن في أندرويد 10+.

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

export function SaveButton({ canvas }: { canvas: HTMLCanvasElement }) {
  if (can('gallery') === 'none') return null;
  return (
    <button onClick={() => canvas.toBlob(async (blob) => {
      if (!blob) return;
      try {
        await saveToGallery(blob, { album: 'My Cards', fileName: 'card.png' });
        toast('Saved to gallery');
      } catch (e) {
        toast('Could not save');
      }
    }, 'image/png')}>Save to gallery</button>
  );
}

احفظ مقطع فيديو وعالج رموز الفشل التي قد يُرفض بها:

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

export async function saveClip(url: string) {
  try {
    await saveToGallery(url, { kind: 'video', album: 'My Clips', fileName: 'clip.mp4' });
    toast('Saved to your Movies folder');
  } catch (e) {
    const code = (e as { code?: string }).code;
    toast(code === 'download-failed' ? 'Check your connection and try again' : 'Could not save the clip');
  }
}

احفظ صورة من رابط بيانات، وسلّمها إلى ورقة المشاركة فقط عند حفظها:

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 أيضًا عنوان data: أو عنوان http(s)؛ مرّر { kind: 'video' } لعنوان فيديو دون نوع MIME للفيديو.

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 فعّل Camera في الخطوة 4 (Access) عند الإنشاء.

استخدمه فقط عندما يجب أن تقع نافذة الكاميرا داخل شاشتك الخاصة؛ وإلا فإن scanCode() أبسط.

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

export function InlineScanner({ onCode }: { onCode: (v: string) => void }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const video = videoRef.current;
    if (!video || can('barcodeScan') === 'none') return;
    const ctl = new AbortController();
    void scanBarcode(video, { signal: ctl.signal, formats: ['qr_code'] }).then((hit) => {
      if (hit) { haptic('success'); onCode(hit.value); }
      else if (!ctl.signal.aborted) setError('Camera not available. Type the code instead.');
    });
    return () => ctl.abort();          // leaving the screen stops the camera
  }, [onCode]);

  return (
    <div>
      <video ref={videoRef} className="aspect-square w-full object-cover" muted playsInline />
      {error && <p role="alert">{error}</p>}
    </div>
  );
}

استخدم الكاميرا الأمامية:

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

declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });

أغلق الماسح بعد أول نتيجة واعرض إدخال رمز يدوي عندما تكون الكاميرا غير متاحة:

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

export function TicketCheck({ onCode }: { onCode: (v: string) => void }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const ctl = useRef<AbortController | null>(null);
  const [typed, setTyped] = useState(false);

  async function start() {
    if (!videoRef.current || can('barcodeScan') === 'none') { setTyped(true); return; }
    ctl.current = new AbortController();
    const hit = await scanBarcode(videoRef.current, { signal: ctl.current.signal, formats: ['ean_13', 'code_128'] });
    if (hit) onCode(hit.value);
    else if (!ctl.current.signal.aborted) setTyped(true);   // null and not cancelled: no camera
  }

  return (
    <div>
      <video ref={videoRef} muted playsInline />
      <button onClick={start}>Scan ticket</button>
      <button onClick={() => ctl.current?.abort()}>Cancel</button>
      {typed && <input placeholder="Type the ticket number" onBlur={(e) => onCode(e.target.value)} />}
    </div>
  );
}

ملاحظات:

  • يستدعي getUserMedia (فيرى المستخدم نافذة إذن الكاميرا) ويفك الترميز بـ BarcodeDetector على كل إطار. تكون can('barcodeScan') هي 'none' حيث يغيب أيٌّ منهما.
  • يفحص الإنشاء كودك: استدعاء scanBarcode( يفعّل الكاميرا لك. وإذا أوقفتها يدويًا فسيفشل طلب الكاميرا وتحصل على null.
  • ابدأه من إجراء للمستخدم (زر مسح أو فتح شاشة مسح)، وليس عند تشغيل التطبيق أبدًا. تعامل مع null كنتيجة عادية مع خيار الإدخال اليدوي.

appmintNative.scanCode appmintNative#

scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>

مثال

يفتح ماسحًا بملء الشاشة ويُحلّ بأول باركود أو رمز QR - استدعاء واحد، ولا شيء ترسمه.

القيم المُعادة: Promise<ScanCodeResult | null> - { value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }، أو null عندما أغلق المستخدم الماسح. أي حالة أخرى تُرفض مع Error قيمة code فيه 'unavailable' (لا ماسح هنا - خدمات Play غير موجودة في التطبيق؛ لا BarcodeDetector أو كاميرا على الويب) أو 'busy' (مسح مفتوح أصلًا) أو 'camera-denied' (الويب: رُفض إذن الكاميرا) أو 'failed'. المتطلبات: لا شيء في التطبيق (ماسح Google لا يحتاج إذن كاميرا).

من زر مسح:

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

export function ScanButton({ onCode }: { onCode: (v: string) => void }) {
  if (can('codeScanner') === 'none') return null;       // nothing can scan here
  return (
    <button
      onClick={async () => {
        try {
          const hit = await scanCode({ formats: ['qr_code'] });
          if (!hit) return;                               // closed: a normal outcome
          haptic('success');
          onCode(hit.value);
        } catch (e) {
          const code = (e as { code?: string }).code;
          toast(code === 'camera-denied' ? 'Allow the camera to scan.' : 'Scanning is not available right now.');
        }
      }}
    >
      Scan QR code
    </button>
  );
}

استخدم الأجزاء المحلَّلة التي يملؤها التطبيق (Wi-Fi، الروابط، جهات الاتصال …)، مع بديل مكتوب يدويًا في ماسح Google:

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

const hit = await scanCode({ manualInput: true });
if (hit?.wifi) console.log('Network', hit.wifi.ssid, hit.wifi.encryption);
else if (hit?.url) window.open(hit.url.url, '_blank');
else if (hit) console.log(hit.type ?? 'text', hit.value);

أجّل المنبّه من زر في شاشة المنبّه الخاصة بك، مستخدمًا onAlarm لتعرف متى يرنّ.

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

export function RingingScreen() {
  const [ringing, setRinging] = useState(false);
  useEffect(() => onAlarm((e) => setRinging(e.action === 'open' && e.ringing === true)), []);
  if (!ringing) return null;
  return (
    <div>
      <button onClick={() => { if (snoozeAlarm('', 10)) setRinging(false); }}>Snooze 10 min</button>
      <button onClick={() => { stopAlarm(); setRinging(false); }}>Stop</button>
    </div>
  );
}

snoozeAlarm يُجيب false عندما لا شيء يرنّ، فافحص قبل أن تعد المستخدم برنّة ثانية.

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

export function snoozeOrExplain(minutes = 0) {
  return snoozeAlarm('', minutes)
    ? `Ringing again in ${minutes || 'a few'} minutes`
    : 'No alarm is ringing right now.';
}

ملاحظات:

  • في التطبيق هذا هو ماسح الرموز من Google (خدمات Play، عبر AppMint.scanCode في الغلاف): تقريب تلقائي، دون طلب إذن الكاميرا. type هو 'url' أو 'wifi' أو 'text' أو 'phone' أو 'email' أو 'sms' أو 'geo' أو 'contact' أو 'calendar' أو 'isbn' أو 'product' أو 'driver_license'، ويُملأ الحقل المطابق. manualInput تتيح للمستخدم كتابة الرمز.
  • على الويب يرسم طبقة كاميرا خاصة بملء الشاشة (زر إغلاق، وإيماءة الرجوع تغلقها) ويفك الترميز بـ BarcodeDetector؛ ولا يُملأ هناك سوى value وformat، ويطلب المتصفح إذن الكاميرا.
  • null تعني دائمًا أن المستخدم أغلق الماسح؛ وكل نتيجة أخرى تُرفض بـ code، فاعرض رسالة في catch.
  • استدعِها من نقرة، وليس عند تحميل الصفحة. حدّد formats عندما تعرفها.

appmintNative.scheduleAlarm appmintNative#

scheduleAlarm(id, input) — @/lib/appmintNative

مثال

يضبط منبّه ساعة حقيقيًا من تطبيق مبني بالذكاء الاصطناعي: يرنّ بمستوى صوت المنبّه في الهاتف (حتى في الوضع الصامت، وعبر عدم الإزعاج عندما يسمح المستخدم بالمنبّهات)، ويستمر بالرنين حتى الإيقاف / التأجيل أو ringSeconds، ويعرض التطبيق على الشاشة فوق شاشة القفل.

القيمة المُعادة: boolean، بشكل متزامن - true عند الضبط؛ وfalse على الويب، وعلى iOS، أو عندما لا تكون alarmSupport() هي 'ready'. المعرّف نفسه يستبدل منبّهًا أقدم. المتطلبات: 🔔 Alarm clock في خطوة Access (الخطوة 4) عند الإنشاء.

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

export function setWeekdayAlarm(hour: number, minute: number): string {
  const support = alarmSupport();
  if (support !== 'ready') return support;       // show the matching fix-it step

  const first = new Date();
  first.setHours(hour, minute, 0, 0);
  if (first.getTime() <= Date.now()) first.setDate(first.getDate() + 1);

  const ok = scheduleAlarm('wake', {
    at: first.getTime(),
    title: 'Wake up',
    repeat: 'weekdays',
    days: [1, 2, 3, 4, 5],     // ISO: Monday = 1 … Sunday = 7
    snoozeMinutes: 9,
    ringSeconds: 300,
  });
  return ok ? 'set' : 'failed';
}

اعرض المنبّه فقط عندما يستطيع التطبيق إطلاق واحد، واشرح ما ينقص خلاف ذلك:

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

const HELP: Record<string, string> = {
  not_enabled: 'Alarm clock is switched off in this build.',
  needs_exact_alarm: 'Allow "Alarms & reminders" for this app in Settings.',
  needs_notification_permission: 'Allow notifications for this app in Settings.',
  unavailable: 'Alarms work only in the Android app.',
};

export function setTimer(minutes: number): string {
  const s = alarmSupport();
  if (s !== 'ready') return HELP[s] ?? 'Alarm not available';
  const ok = scheduleAlarm('timer', {
    at: Date.now() + minutes * 60_000,
    title: 'Timer done',
    snoozeMinutes: 0,          // hides the Snooze button
    ringSeconds: 60,
  });
  return ok ? `Timer set for ${minutes} min` : 'Could not set the timer';
}

تفاعل مع طريقة انتهاء منبّه يرنّ، وأوقفه من شاشتك:

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

export function useAlarmScreen() {
  const [ringing, setRinging] = useState(isAlarmRinging());
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open') setRinging(!!e.ringing);
    else if (e.action === 'dismiss' || e.action === 'snooze' || e.action === 'missed') setRinging(false);
    else if (e.action === 'error') console.warn('Alarm error', e.error);
  }), []);
  return {
    ringing,
    stop: () => stopAlarm(),            // '' = whichever alarm is ringing
    snooze: () => snoozeAlarm('', 5),   // ring again in 5 minutes
  };
}

ملاحظات: لتطبيق Android فقط. يحافظ repeat يوميًا / أسبوعيًا / أيام الأسبوع / شهريًا / سنويًا على نفس وقت الساعة عبر تغييرات التوقيت الصيفي. ألغِ باستخدام 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 على الويب - لا توجد واجهة ويب). speak() ← Promise<void>. onChange(cb) ← دالة إلغاء اشتراك؛ ويُشغَّل cb(enabled) عند تشغيل قارئ الشاشة أو إيقافه (في التطبيق فقط). المتطلبات: لا شيء.

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

function useScreenReader() {
  const [on, setOn] = useState(false);
  useEffect(() => {
    void screenReader.isEnabled().then(setOn);
    return screenReader.onChange(setOn);
  }, []);
  return on;   // e.g. skip auto-advancing carousels when true
}

async function onPaid() {
  await screenReader.speak('Payment complete', 'en');
}

أعلن النتيجة فقط للأشخاص الذين يستخدمون قارئ الشاشة، واترك الرسائل المرئية للجميع:

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

export async function notifySaved() {
  if (await screenReader.isEnabled()) await screenReader.speak('Note saved', 'en');
  else toast('Note saved');
}

أزل حركة أثناء تشغيل TalkBack، وتوقف عن الاستماع عندما يزول المكوّن:

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

export function Banner({ text }: { text: string }) {
  const [reduce, setReduce] = useState(false);
  useEffect(() => {
    let alive = true;
    void screenReader.isEnabled().then((v) => { if (alive) setReduce(v); });
    const off = screenReader.onChange(setReduce);   // returns an unsubscribe function
    return () => { alive = false; off(); };
  }, []);
  return <div className={reduce ? '' : 'animate-pulse'} role="status" aria-live="polite">{text}</div>;
}

ملاحظات: في التطبيق يمر speak عبر مكوّن ScreenReader؛ وعلى الويب (أو إذا فشل المكوّن) يُنطق بتحويل النص إلى كلام. فضّل ترميزًا جيدًا (aria-label ومناطق aria-live) لمعظم الإعلانات؛ واستخدم speak للتغييرات القصيرة المهمة.

appmintNative.searchContacts appmintNative#

searchContacts(query: string, limit = 50): Promise<Contact[]>

مثال

يجد جهات الاتصال التي تطابق كلمة بحث. للتطبيق فقط.

القيمة المُرجعة: Promise<Contact[]> - فارغة عندما لا يطابق شيء، أو يُرفض الوصول، أو تكون جهات الاتصال معطّلة. المتطلبات: مثل listContacts: جهات الاتصال في الخطوة 4 (الوصول) (يفعّلها الإنشاء عندما يستدعي كودك هذه الدالة)، وإذن المستخدم عند أول استخدام.

مربع بحث بتأخير صغير أثناء الكتابة.

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

let timer: ReturnType<typeof setTimeout> | undefined;
let latest = 0;

export function onSearchInput(text: string, show: (names: string[]) => void) {
  if (can('contactList') === 'none') return;
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const mine = ++latest;
    const found = await searchContacts(text, 20).catch(() => []);
    if (mine !== latest) return;                     // a newer search already started
    show(found.map((c) => c.name ?? ''));
  }, 250);
}

اعرض حالة واضحة عندما لا تكون جهات الاتصال متاحة وعندما لا يطابق شيء:

import { useState } from 'react';
import { can, searchContacts, type Contact } from '@/lib/appmintNative';

export function ContactPicker({ onPick }: { onPick: (c: Contact) => void }) {
  const [rows, setRows] = useState<Contact[] | null>(null);
  if (can('contactList') === 'none') return <p>Choosing a contact works only in the Android app.</p>;
  return (
    <div>
      <input placeholder="Search contacts" onChange={async (e) => setRows(await searchContacts(e.target.value, 10))} />
      {rows && rows.length === 0 && <p>No contacts match, or access was not allowed.</p>}
      {rows?.map((c) => <button key={c.id ?? c.name} onClick={() => onPick(c)}>{c.name} {c.phones?.[0]}</button>)}
    </div>
  );
}

ابحث عن شخص بالاسم، ثم املأ مسبقًا رسالة نصية إليه:

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

export async function textPerson(name: string, message: string): Promise<boolean> {
  const [first] = await searchContacts(name, 1);
  const number = first?.phones?.[0];
  return number ? composeSms(number, message) : false;   // the user presses send
}

ملاحظات: تتم المطابقة بواسطة بحث جهات الاتصال في الهاتف عن الأسماء. كل جهة اتصال هي { id?, name?, phones?: string[], emails?: string[], photoUri? }. على الويب can('contactList') هي 'none' - لا توجد واجهة ويب لقراءة دفتر العناوين. جهات الاتصال بيانات شخصية: صرّح بها في نموذج 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' (Secure Keys ليست في هذه النسخة / لا 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 غير قابل للاستخراج على الويب. إزالة المفتاح هي "الحذف الآمن" الحقيقي الوحيد.

القيمة المُعادة: جميعها Promises. generate(alias, {requireAuth?, authValiditySeconds?}) → {hardwareBacked}؛ encrypt(alias, text) → سلسلة base64؛ decrypt(alias, base64) → النص؛ remove(alias) / has(alias) / isHardwareBacked(alias) → قيمة منطقية؛ list() → string[]. generate وencrypt وdecrypt ترمي SecureKeyError مع code. المتطلبات: Secure Keys (encryption) في الخطوة 4 (Access) عند الإنشاء.

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

export async function saveDiary(text: string) {
  if (can('secureKeys') === 'none') throw new Error('Secure storage is not available here');
  if (!(await secureKeys.has('diary'))) {
    const { hardwareBacked } = await secureKeys.generate('diary', { requireAuth: true, authValiditySeconds: 60 });
    console.log(hardwareBacked ? 'Key in secure hardware' : 'Key in software');
  }
  const sealed = await secureKeys.encrypt('diary', text);    // strings in, base64 out
  localStorage.setItem('diary', sealed);
}

export async function readDiary(): Promise<string | null> {
  const sealed = localStorage.getItem('diary');
  if (!sealed) return null;
  try {
    return await secureKeys.decrypt('diary', sealed);
  } catch (e) {
    if (e instanceof SecureKeyError && e.code === 'key-invalidated') return null;  // data is gone
    throw e;
  }
}

احذف فعليًا - لن يستطيع أحد قراءة البيانات المختومة بعد هذا:

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

export async function wipeDiary() {
  await secureKeys.remove('diary');
  localStorage.removeItem('diary');
}

اسرد المفاتيح التي يحتفظ بها التطبيق وأظهر هل كل منها محمي بالعتاد (تُجيب الويب دائمًا false):

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

export async function describeKeys(): Promise<string[]> {
  if (can('secureKeys') === 'none') return [];
  const aliases = await secureKeys.list();
  return Promise.all(aliases.map(async (alias) =>
    `${alias}: ${(await secureKeys.isHardwareBacked(alias)) ? 'secure hardware' : 'software'}`));
}

ملاحظات: can('secureKeys') هي 'native' في التطبيق و'web' في المتصفح، حيث تكون isHardwareBacked() دائمًا false - لا تقل "عتاد" هناك أبدًا. مفتاح requireAuth: في التطبيق تطلق encrypt/decrypt الخطأ auth-required دون عرض أي شيء بعد انقضاء نافذة الفتح - استدعِ unlock() وحاول مجددًا؛ وعلى الويب تعرض بيئة التشغيل unlock() بنفسها. شفّر سجلًا واحدًا في كل مرة (يحدّ التطبيق الاستدعاء بـ 4 ميغابايت). الأسماء المستعارة: أحرف وأرقام و_ - .، بحد أقصى 64.

appmintNative.sendSms appmintNative#

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

مثال

يرسل رسالة SMS مباشرة، دون شاشة تأكيد. تطبيق Android فقط.

القيمة المُعادة: Promise<boolean> - true عندما يبلّغ الهاتف أن الرسالة أُرسلت، false عند الفشل، أو عدم تفعيل SMS، أو رفض المستخدم للإذن. المتطلبات: مفتاح SMS (read & send) في الخطوة 4 (Access) - تفعّله النسخة المبنية عندما يستدعي كودك هذا - وإذن المستخدم عند أول استخدام.

أرسل تنبيهًا وأخبر المستخدم بما حدث.

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

export async function sendAlert(to: string, text: string): Promise<string> {
  if (can('smsSend') === 'none') return 'Sending SMS works only in the Android app.';
  try {
    const ok = await sendSms(to, text);
    return ok ? 'Alert sent' : 'The SMS could not be sent';
  } catch {
    return 'No answer from the phone after 3 minutes';
  }
}

اطلب تأكيدًا أولًا، لأن الرسالة تُرسل دون شاشة لإلغائها:

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

export async function sendCheckIn(to: string) {
  if (can('smsSend') === 'none') { composeSms(to, 'I am safe'); return; }   // the user presses send
  if (!window.confirm('Send "I am safe" to ' + to + '? This may cost money.')) return;
  toast((await sendSms(to, 'I am safe')) ? 'Sent' : 'Could not send. Check the SMS permission.');
}

أرسل التنبيه نفسه إلى عدة أشخاص واحدًا تلو الآخر، واحسب ما تم إرساله:

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

export async function alertAll(numbers: string[], text: string) {
  let sent = 0;
  for (const n of numbers) {
    if (await sendSms(n, text)) sent += 1;      // waits for the phone's answer for each one
  }
  return { sent, failed: numbers.length - sent };
}

ملاحظات: تنتهي الأداة عند أول إجابة، وهي sent (غادرت الرسالة الهاتف)، وليس delivered. لتقارير التسليم استخدم WebToApk.sendSms الخام مع الحدث appmint:sms. can('smsSend') هي 'none' على الويب (لا يستطيع أي متصفح إرسال SMS). قد تكلف كل رسالة SMS المستخدم مالًا. SMS إذن مقيّد في Google Play: تحتاج إلى Permissions Declaration معتمد في Play Console، لذا فضّل composeSms() عندما يستطيع المستخدم الضغط على إرسال.

appmintNative.setInAppPresentation appmintNative#

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

مثال

يختار من يرسم رسالة داخل التطبيق (حملات المنشئ): التطبيق ('auto'، الافتراضي: تصميم المنشئ للافتة أو البطاقة أو الورقة أو ملء الشاشة) أو واجهتك الخاصة ('none'، عبر onInAppMessage).

القيم المُعادة: boolean بشكل متزامن: true عند الحفظ (يبقى على الهاتف). دائمًا false على الويب وبدون AppMint Push. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

// The app draws its own messages from now on.
setInAppPresentation('none');

مفتاح في الإعدادات:

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

export function onNativePopupsChange(on: boolean) {
  if (!setInAppPresentation(on ? 'auto' : 'none')) console.info('In-app messages are not available here');
}

تولَّ الأمر أثناء فتح شاشة واحدة، ثم أعده:

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

export function useQuietScreen() {
  useEffect(() => {
    setInAppPresentation('none');
    return () => { setInAppPresentation('auto'); };
  }, []);
}

ملاحظات: الرسالة التي تُسلَّم إلى كودك تُحتسب كمعروضة. كما أن ضبط 'none' يغلق أيضًا الرسالة المعروضة على الشاشة الآن.

appmintNative.setMusicVolume appmintNative#

setMusicVolume(percent) — @/lib/appmintNative

مثال

يضبط مستوى الموسيقى الخلفية المتكررة للتطبيق من 0 إلى 100 (يُقرَّب ويُقيَّد تلقائيًا).

القيم المُعادة: void. المتطلبات: ملف مختار ضمن الموسيقى الخلفية في الخطوة 3 (التكامل) عند الإنشاء. لا يفعل شيئًا عندما تكون musicVolume() -1.

يحتفظ التطبيق بالمستوى للتشغيل التالي، لذا يكفيك مفتاح كتم:

import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40);   // remembered by the app

شريط تحكم بالصوت يظهر فقط عندما يتضمن التطبيق موسيقى:

import { useState } from 'react';
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export function MusicSlider() {
  const [level, setLevel] = useState(musicVolume());
  if (level < 0) return null;                       // no music in this app: draw no control
  return (
    <input type="range" min={0} max={100} value={level}
      onChange={(e) => { const v = Number(e.target.value); setLevel(v); setMusicVolume(v); }} />
  );
}

اخفض صوت الموسيقى أثناء تشغيل فيديو وأعده بعد ذلك:

import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

let before = -1;
export function duckMusic() { before = musicVolume(); if (before > 0) setMusicVolume(10); }
export function restoreMusic() { if (before >= 0) setMusicVolume(before); before = -1; }

ملاحظات: يجعل 0 الموسيقى صامتة لكنها تستمر في العمل. يُحفظ المستوى على الهاتف (حتى يمسح المستخدم بيانات التطبيق). تطبيق Android فقط؛ وعلى الويب لا يحدث شيء.

appmintNative.setPushPresentation appmintNative#

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

مثال

يختار من يرسم رسالة AppMint Push أرسلتها بنمط داخل التطبيق (شريط، نافذة منبثقة، ورقة سفلية أو ملء الشاشة) بينما التطبيق مفتوح: غلاف التطبيق ('auto'، وهو الافتراضي) أو كودك الخاص ('none').

القيم المُعادة: boolean بشكل متزامن - true عند حفظ الوضع. دائمًا false على الويب. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

import { useEffect } from 'react';
import { setPushPresentation, pushInbox } from '@/lib/appmintNative';

// Option A — keep the shell's banner, and never draw a second one on top of it.
export function usePushBanner(show: (title: string) => void) {
  useEffect(() => {
    const onPush = (e: Event) => {
      const d = (e as CustomEvent).detail;
      // { type: 'received', message, presented } — presented = the shell already showed it
      if (d.type === 'received' && !d.presented) show(d.message.title);
    };
    window.addEventListener('appmint:push', onPush);
    return () => window.removeEventListener('appmint:push', onPush);
  }, [show]);
}

// Option B — the app draws every message itself.
export function takeOverPushBanners() {
  setPushPresentation('none');   // saved on the phone; 'auto' gives it back to the shell
  return pushInbox();
}

تحقق من الإجابة، فهي false على الويب أو عندما يكون AppMint Push متوقفًا:

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

export function useOwnBanners(on: boolean): boolean {
  const saved = setPushPresentation(on ? 'none' : 'auto');
  if (!saved) console.info('Push banners are not controllable here');
  return saved;
}

ارسم لافتتك الخاصة لكل رسالة وأبقِ الصندوق الوارد متوافقًا:

import { useEffect, useState } from 'react';
import { setPushPresentation, markPushRead, pushInbox, type PushMessage } from '@/lib/appmintNative';

export function useCustomBanner() {
  const [msg, setMsg] = useState<PushMessage | null>(null);
  useEffect(() => {
    setPushPresentation('none');                  // the shell draws nothing; the page does
    const onPush = (e: Event) => {
      const d = (e as CustomEvent).detail;
      if (d.type === 'received') setMsg(d.message);
      if (d.type === 'revoked') setMsg((m) => (m?.id === d.id ? null : m));
    };
    window.addEventListener('appmint:push', onPush);
    return () => { window.removeEventListener('appmint:push', onPush); setPushPresentation('auto'); };
  }, []);
  return { msg, unread: pushInbox().filter((m) => !m.read).length, dismiss: () => { if (msg) markPushRead(msg.id); setMsg(null); } };
}

ملاحظات: صندوق الوارد وحدث appmint:push يعملان بالطريقة نفسها في الوضعين. كما أن ضبط 'none' يغلق أيضًا رسالة يعرضها الغلاف الآن.

appmintNative.setPushTag appmintNative#

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

مثال

يضبط وسمًا واحدًا من AppMint Push على هذا التثبيت. يستطيع مركز الإرسال الإرسال إلى كل من لديه وسم، ويمكن لأتمتة الوسوم إرسال رسالة بعد مدة محددة من ضبط الوسم. '' تزيل الوسم.

القيم المُعادة: boolean بشكل متزامن - true عند الحفظ؛ وfalse عندما لا يكون المفتاح من 1 إلى 32 حرفًا أو رقمًا أو _، أو تتجاوز القيمة 128 حرفًا أو يتجاوز التثبيت 50 وسمًا. دائمًا false على الويب. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء؛ ويصل الاستهداف بالوسوم والأتمتة إلى التطبيقات المنشأة من 30 سبتمبر 2026 فصاعدًا.

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

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

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

تحقق من النتيجة: false تعني أن الوسم قد رُفض (مفتاح غير صالح، أو قيمة تتجاوز 128 حرفًا، أو أكثر من 50 وسمًا) أو أنك على الويب، لذا احتفظ بسجلّك الخاص للحالة.

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

export function markNewsletterChoice(subscribed: boolean) {
  const saved = setPushTag('newsletter', subscribed ? 'yes' : '');
  if (!saved) console.warn('Push tag not saved (web, Push off, or invalid key/value)');
  return saved;
}

ضع وسمًا على هاتف عندما يبلغ إنجازًا، ليستهدف مركز الإرسال كل من وصل إلى ذلك المستوى.

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

export function useLevelTag(level: number) {
  useEffect(() => {
    if (level >= 10) setPushTag('veteran', 'yes');
    setPushTag('level', String(level));
  }, [level]);
}

ملاحظات: الوسوم تخص التثبيت وليس المستخدم المسجَّل دخوله. لا تضع بيانات خاصة في وسم أبدًا.

appmintNative.setPushTags appmintNative#

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

مثال

يضبط عدة وسوم لـ AppMint Push بتغيير واحد؛ وقيمة '' تزيل ذلك الوسم، أما الوسوم التي لا تسمّيها فتبقى.

القيمة المُرجعة: boolean بشكل متزامن - true عند الحفظ؛ false عندما يخالف أي مفتاح أو قيمة قواعد setPushTag (عندها لا يُحفظ شيء). دائمًا false على الويب. المتطلبات: أندرويد: تفعيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء؛ ويصل الاستهداف بالوسوم والأتمتة إلى التطبيقات المنشأة اعتبارًا من 30 سبتمبر 2026.

import { setPushTags, getPushTags, setPushUser } from '@/lib/appmintNative';

export function tagProfile(profile: { plan: 'free' | 'pro'; level: number; team?: string }) {
  return setPushTags({
    plan: profile.plan,
    level: String(profile.level),   // values are text
    team: profile.team ?? '',       // '' removes the tag
  });
}

// Sign-out: clear every tag and the user id.
export function signOutOfPush() {
  const cleared = Object.fromEntries(Object.keys(getPushTags()).map((k) => [k, '']));
  setPushTags(cleared);
  setPushUser('');
}

ضع على الهاتف وسمًا بلغته المختارة ومواضيعه عندما يحفظ المستخدم إعداداته، وتحقق من قبول كل وسم.

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

export function savePreferences(prefs: { lang: string; topics: string[] }) {
  const ok = setPushTags({
    lang: prefs.lang,
    topics: prefs.topics.join(',').slice(0, 128),   // a value is at most 128 characters
    onboarded: 'yes',
  });
  if (!ok) console.warn('Push tags not saved: nothing was changed');
  return ok;
}

أبقِ الوسوم متوافقة مع الحساب المسجَّل دخوله في React 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 على الويب. المتطلبات: Android: تشغيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء؛ ويصل الإرسال إلى مستخدم إلى التطبيقات المنشأة من 30 سبتمبر 2026 فصاعدًا.

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

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

تعامل مع معرّف مرفوض: تجيب setPushUser بـ false لعنوان بريد إلكتروني أو أي معرّف يحوي أحرفًا خارج المجموعة المسموحة، فأرسل معرّفًا ثابتًا بدلًا منه.

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

export function linkPushToAccount(account: { id: string; email: string }) {
  const ok = setPushUser(account.id);   // the id, never the email
  if (!ok) console.warn('Push user id not saved (web, Push off, or invalid characters)');
  return ok;
}

امسح المعرّف عندما يسجّل المستخدم خروجه، في معالج نقر عادي.

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

export function SignOutButton({ signOut }: { signOut: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await signOut();
        if (getPushUser()) setPushUser('');   // this install stops receiving "send to this user" messages
      }}
    >
      Sign out
    </button>
  );
}

اجمعه مع وسم بحيث تحدد خطوة تسجيل دخول واحدة المستخدم وخطته معًا.

import { setPushUser, setPushTag } from '@/lib/appmintNative';

export function onSignedIn(userId: string, plan: 'free' | 'pro') {
  setPushUser(userId);
  setPushTag('plan', plan);
}

ملاحظات: المعرّف يخص التثبيت، لذا يجب أن يمسحه تسجيل الخروج. وهو لا يمسح الوسوم - استدعِ setPushTags بقيم '' للوسوم الخاصة بالمستخدم.

appmintNative.setStatusBar appmintNative#

setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void

مثال

يلوّن شريط حالة النظام ليطابق الشاشة، ويختار له أيقونات فاتحة أو داكنة.

يعيد: لا شيء. يتطلب: لا شيء.

style هو لون الأيقونات: 'light' = أيقونات فاتحة لشريط داكن (الافتراضي)، 'dark' = أيقونات داكنة لشريط فاتح. استدعِه مرة واحدة عند البدء مع --bg الخاص بالتطبيق، ومرة أخرى في أي شاشة ذات خلفية مختلفة:

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

export function App() {
  useEffect(() => { setStatusBar('#0B0F1A', 'light'); }, []);   // dark app, light icons
  return <main>{/* … */}</main>;
}

export function PhotoViewer() {
  useEffect(() => {
    setStatusBar('#000000', 'light');
    return () => setStatusBar('#0B0F1A', 'light');                // restore on leave
  }, []);
  return null;
}

شاشة فاتحة:

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

setStatusBar('#FFFFFF', 'dark');

الشكل بأسلوب Capacitor (يُسمّى النمط على اسم الخلفية: 'DARK' = أيقونات فاتحة):

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

await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });

ملاحظات:

  • في تطبيق أندرويد يلوّن الغلاف الشريط بنفسه، وهو ما يعمل أيضًا على أندرويد 15 بحافة إلى حافة؛ ويُتجاهل اللون غير القابل للتحليل - مرّر #RRGGBB. الاستدعاء بالنمط فقط (backgroundColor = null) يُبقي اللون الذي ضبطته هذه الصفحة آخر مرة ويقلب الأيقونات؛ وقبل أن تضبط الصفحة أي لون يقلب الأيقونات عبر إضافة StatusBar ويبقى الشريط باللون المختار وقت الإنشاء.
  • على الويب يضبط <meta name="theme-color">؛ ويختار المتصفح لون الأيقونة بنفسه.

appmintNative.setVolume appmintNative#

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

مثال

يضبط مستوى صوت الهاتف نفسه لمسار صوتي واحد، من 0 إلى 100. showUi يعرض شريط مستوى الصوت في الهاتف أثناء تحركه.

القيمة المُعادة: { ok, stream?, current?, max?, percent?, error? } - الخطأ هو 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 عندما أغلقها المستخدم أو كانت المشاركة غير متاحة. يُرفض عندما تكون المشاركة نفسها غير صالحة (مثل رابط غير مدعوم) أو عندما تكون مشاركة أخرى ما زالت مفتوحة. المتطلبات: لا شيء.

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

ملاحظات:

  • مرّر الحقول التي لديك فقط. في التطبيق يمر النص والروابط عبر إضافة Share الخاصة بـ Capacitor.
  • files يعمل في تطبيق Android (تذهب الملفات إلى ورقة المشاركة كمرفقات حقيقية؛ ويُضاف url إلى النص) وعلى الويب حيث يستطيع المتصفح مشاركة الملفات (navigator.canShare({ files })). أبقِ الملفات صغيرة - فهي تُمرَّر إلى التطبيق في الذاكرة.
  • على الويب استدعِها من نقرة؛ فالمتصفحات ترفض مشاركة لم يبدأها إجراء من المستخدم.

appmintNative.Share appmintNative#

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

مثال

موثَّق مع appmintNative.share - والمثال هناك يعرض هذا أيضًا.

يفتح ورقة مشاركة النظام في التطبيق، وWeb Share API في المتصفح.

القيم المُعادة: Promise<boolean> - تكون true عند فتح الورقة؛ وfalse عندما أغلقها المستخدم أو كانت المشاركة غير متاحة. يُرفض عندما تكون المشاركة نفسها غير صالحة (مثل رابط غير مدعوم) أو عندما تكون مشاركة أخرى ما زالت مفتوحة. المتطلبات: لا شيء.

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

ملاحظات:

  • مرّر الحقول التي لديك فقط. في التطبيق يمر النص والروابط عبر إضافة Share الخاصة بـ Capacitor.
  • 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 بالميلي ثانية منذ 1970. المتطلبات: خيار الإنشاء استقبال المشاركات من تطبيقات أخرى مع "النصوص والروابط" - ويُفعَّل تلقائيًا عندما يستدعي كودك sharedText( أو onShared(.

افحص عند التحميل، واحفظه، ثم امسحه:

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().
  • صورة أو فيديو تمت مشاركته مع تعليق: يصل التعليق هنا، ويصل الملف عبر واجهة الملف المفتوح.
  • تطبيق Android فقط. can('shareTarget') هي 'none' على الويب، وsharedText() تُعيد null هناك.

appmintNative.sheet appmintNative#

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

مثال

يفتح ورقة سفلية نمطية بسلوك الورقة الحقيقي - للوحة تصفية أو محرّر أو معاينة تفاصيل.

القيمة المُعادة: مقبض: close() يغلقها، وelement هو عنصر المتن (املأه أو حدّثه)، وclosed هو Promise<void> يتم عند إغلاق الورقة، بأي طريقة أُغلقت. المتطلبات: لا شيء.

الخيارات: title (عنوان اختياري)، content - عنصر أو نص عادي (ليس HTML) أو دالة (host) => { … } تملأ المتن؛ dismissible (الافتراضي true: النقر على الخلفية، السحب لأسفل، Escape وإيماءة الرجوع تغلقها).

محتوى DOM عادي:

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

const h = sheet({
  title: 'About this list',
  content: (host) => {
    const p = document.createElement('p');
    p.textContent = 'Items sync to all your devices.';
    host.appendChild(p);
  },
});
await h.closed;

محتوى React، يُعرض داخل الورقة عبر 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 ووعد closed.

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

export async function confirmDelete(name: string): Promise<boolean> {
  let answer = false;
  const h = sheet({
    title: 'Delete this item?',
    dismissible: false,                       // only the buttons below close it
    content: (host) => {
      const p = document.createElement('p');
      p.textContent = `"${name}" will be removed.`;
      const yes = document.createElement('button');
      yes.textContent = 'Delete';
      yes.onclick = () => { answer = true; h.close(); };
      const no = document.createElement('button');
      no.textContent = 'Keep it';
      no.onclick = () => h.close();
      host.append(p, yes, no);
    },
  });
  await h.closed;                             // resolves however the sheet closed
  return answer;
}

ملاحظات:

  • يُرسم بعناصر تصميم التطبيق (--surface، --ink، --muted، --border)، فوق المنطقة الآمنة، مع اهتزاز خفيف عند الفتح.
  • السحب لأسفل يغلقها عند سحبها أكثر من 120 px أو بحركة سريعة؛ ويتمرر المحتوى أولًا عندما لا يكون في الأعلى.
  • مع dismissible: false لا يغلقها إلا close() الخاص بك - أعطِ المستخدم زرًا.
  • لقائمة إجراءات بسيطة استخدم actionSheet().

appmintNative.shortcuts appmintNative#

shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()

مثال

يضيف اختصارات إلى قائمة الضغط المطوّل على أيقونة التطبيق (اختصارات Android الديناميكية). تفتح النقرة التطبيق عند ذلك المسار.

القيمة المُرجعة: set() تُرجع Promise<boolean> - true عندما سُلّمت الاختصارات إلى النظام، false حيث لا توجد اختصارات (الويب). clear() تُرجع Promise<void>. المتطلبات: لا شيء.

عيّنها مرة واحدة عند البدء، ووجّه بحسب رابط التشغيل:

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { can, shortcuts, launchUrl } from '@/lib/appmintNative';

export function useAppShortcuts() {
  const navigate = useNavigate();
  useEffect(() => {
    if (can('shortcuts') !== 'none') {
      void shortcuts.set([
        { id: 'new', title: 'New note', route: '/new' },
        { id: 'search', title: 'Search', route: '/search' },
      ]);
    }
    void launchUrl().then((u) => { if (u) navigate(new URL(u).pathname); });
  }, [navigate]);
}

أزلها (مثلًا بعد تسجيل الخروج):

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

await shortcuts.clear();

افتح الشاشة المطابقة عند نقر اختصار أثناء تشغيل التطبيق، بـ onDeepLink.

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

export function useShortcutRouting() {
  const navigate = useNavigate();
  useEffect(() => onDeepLink((url) => navigate(new URL(url).pathname)), [navigate]);
}

اعرض الاختصارات لمستخدم مسجّل الدخول فقط، وعالج حالة الويب، حيث تُجيب set() false.

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

export function useAccountShortcuts(signedIn: boolean) {
  useEffect(() => {
    if (!signedIn) { void shortcuts.clear(); return; }
    void shortcuts.set([
      { id: 'inbox', title: 'Inbox', route: '/inbox' },
      { id: 'compose', title: 'Write a message', route: 'compose' },   // gets a leading "/"
    ]).then((handed) => { if (!handed) console.info('No launcher shortcuts on this platform'); });
  }, [signedIn]);
}

ملاحظات:

  • أربعة عناصر على الأكثر؛ وتُسقَط العناصر الزائدة. تُضاف / البادئة إلى route إذا لم تكن موجودة. id اختياري.
  • Android 7.1 أو أحدث؛ وعلى Android الأقدم لا يفعل الاستدعاء شيئًا. تُقطع العناوين إلى 25 حرفًا (التسمية القصيرة) و50 (التسمية الطويلة).
  • تفتح النقرة التطبيق برابط الإطلاق appmint-shortcut://<package><route>، لذلك يكون new URL(u).pathname هو مسارك. اقرأه بـ launchUrl() عند التركيب وبـ onDeepLink() أثناء عمل التطبيق.
  • can('shortcuts') تكون 'none' على الويب: المتصفح لا يملك أيقونة تطبيق.

appmintNative.snoozeAlarm appmintNative#

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

مثال

يوقف المنبّه الذي يرنّ ويعيد تشغيله بعد minutes (0 أو بدونها = snoozeMinutes الخاصة بالمنبّه، الافتراضي 10). ولا يتحرك رنين الغد للمنبّه المتكرر.

القيم المُعادة: boolean - تكون true عندما كان يرنّ وتم تأجيله. المتطلبات: 🔔 منبّه الساعة في خطوة الوصول (الخطوة 4) عند الإنشاء.

import { isAlarmRinging, snoozeAlarm } from '@/lib/appmintNative';

// A shake-to-snooze handler:
window.addEventListener('devicemotion', (e) => {
  const a = e.accelerationIncludingGravity;
  if (a && Math.abs(a.x ?? 0) > 25 && isAlarmRinging()) snoozeAlarm('', 5);
});

أجّل المنبّه من زر في شاشة المنبّه الخاصة بك، مستخدمًا onAlarm لتعرف متى يرنّ.

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

export function RingingScreen() {
  const [ringing, setRinging] = useState(false);
  useEffect(() => onAlarm((e) => setRinging(e.action === 'open' && e.ringing === true)), []);
  if (!ringing) return null;
  return (
    <div>
      <button onClick={() => { if (snoozeAlarm('', 10)) setRinging(false); }}>Snooze 10 min</button>
      <button onClick={() => { stopAlarm(); setRinging(false); }}>Stop</button>
    </div>
  );
}

snoozeAlarm يُجيب false عندما لا شيء يرنّ، فافحص قبل أن تعد المستخدم برنّة ثانية.

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

export function snoozeOrExplain(minutes = 0) {
  return snoozeAlarm('', minutes)
    ? `Ringing again in ${minutes || 'a few'} minutes`
    : 'No alarm is ringing right now.';
}

appmintNative.speak appmintNative#

speak(text, opts?) — @/lib/appmintNative

مثال

يقرأ النص بصوت عالٍ بمحرك تحويل النص إلى كلام في الهاتف.

القيمة المُرجعة: void - يبدأ النطق (أو يُضاف إلى الطابور بعد الكلام الحالي) ولا يعطي إشارة "انتهى". opts: { lang?: string; rate?: number; pitch?: number } (الافتراضيات: لغة المحرك، 1، 1). المتطلبات: لا شيء.

import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadAloud({ text }: { text: string }) {
  return (
    <div>
      <button onClick={() => speak(text, { lang: 'en-IN', rate: 0.9 })}>Read aloud</button>
      <button onClick={() => stopSpeaking()}>Stop</button>
    </div>
  );
}

لغات وسرعات مختلفة:

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

speak('வணக்கம்', { lang: 'ta-IN' });          // Tamil
speak('Slow and clear', { rate: 0.7 });       // rate 0.1..4, 1 = normal
speak('High voice', { pitch: 1.5 });          // pitch 0.1..4, 1 = normal

انطق تأكيدًا قصيرًا بعد إجراء، وأخبر المستخدم نصيًا أيضًا.

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

export function announceSaved(title: string) {
  toast('Saved');
  speak(`${title} saved`, { rate: 1.1 });
}

اقرأ قائمة عنصرًا بعد عنصر؛ يضع speak() في طابور، لذا استدعه في حلقة وأوقف كل شيء بـ stopSpeaking() عند إغلاق الشاشة.

import { useEffect } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadHeadlines({ items }: { items: string[] }) {
  useEffect(() => () => stopSpeaking(), []);   // leaving the screen stops the voice
  return <button onClick={() => items.forEach((t) => speak(t))}>Read all</button>;
}

ملاحظات: في تطبيق Android: المحرك الأصلي للتطبيق. على الويب: speechSynthesis. تُشغَّل عدة استدعاءات speak() واحدًا تلو الآخر. إذا احتجت إلى معرفة متى ينتهي الكلام (لإبراز الكلمات أو الانتقال)، فاستخدم window.speechSynthesis مع onend الخاص بنطق مباشرة؛ وهو يعمل في تطبيق Android أيضًا.

appmintNative.speech appmintNative#

speech — @/lib/appmintNative

مثال

الإملاء: يحوّل كلام المستخدم إلى نص. مُتعرّف الكلام في أندرويد داخل تطبيق أندرويد، و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 }. المتطلبات: Mic في الخطوة 4 (Access)؛ تفعّله البنايات المنشأة بالذكاء الاصطناعي تلقائيًا عندما يستخدم الكود التعرف على الكلام.

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

حرّره مرة واحدة، من hook، بعد ظهور بيانات الشاشة الأولى.

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' خلفية داكنة (أيقونات فاتحة)، وأي قيمة أخرى تعني أيقونات داكنة. في تطبيق Android يرسم جسر الغلاف الشريط (استدعاء اللون في المكوّن لا يفعل شيئًا على Android 15+)، ويقلب setStyle() الأيقونات بحسب لون الشريط الحالي. على الويب يصبح اللون <meta name="theme-color">.

appmintNative.stopAlarm appmintNative#

stopAlarm(id?) — @/lib/appmintNative

مثال

يوقف منبّه الساعة الذي يرنّ، مثل زر الإغلاق فيه. يبقى المنبّه المتكرر مضبوطًا لموعده التالي.

القيم المُعادة: boolean - true عندما كان يرنّ وتوقف الآن. إذا حُذف id فيُوقف ما يرنّ منها. المتطلبات: 🔔 منبّه الساعة في خطوة الوصول (الخطوة 4) عند الإنشاء.

import { stopAlarm, snoozeAlarm } from '@/lib/appmintNative';

export function RingScreen({ id, onClose }: { id: string; onClose: () => void }) {
  return (
    <div className="ring">
      <button onClick={() => { snoozeAlarm(id, 10); onClose(); }}>Snooze 10 min</button>
      <button onClick={() => { stopAlarm(id); onClose(); }}>Stop</button>
    </div>
  );
}

أوقف المنبّه من الشاشة التي يفتحها التطبيق عند رنين المنبّه، وتفاعل مع كل طريقة ينتهي بها:

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

export function useRingingAlarm() {
  const [ringingId, setRingingId] = useState<string | null>(null);
  useEffect(() => onAlarm((e) => {
    if (e.action === 'open' && e.ringing) setRingingId(e.id);
    else setRingingId(null);   // dismiss, snooze, missed or error: it is no longer ringing
  }), []);
  return {
    ringingId,
    stop: () => { if (isAlarmRinging()) stopAlarm(ringingId ?? ''); },
  };
}

افحص الإجابة: false تعني أن شيئًا لم يكن يرنّ (أو أنك على الويب)، فلا تدّعِ أنه توقف:

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

export function stopWithFeedback(id: string, say: (m: string) => void) {
  if (alarmSupport() === 'unavailable') return say('Alarms only work in the Android app');
  say(stopAlarm(id) ? 'Alarm stopped' : 'That alarm is not ringing');
}

appmintNative.stopSpeaking appmintNative#

stopSpeaking() — @/lib/appmintNative

مثال

يوقف تحويل النص إلى كلام الآن ويُسقط كل ما ينتظر النطق.

القيمة المُرجعة: void. المتطلبات: لا شيء.

import { useEffect } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function StoryPage({ story }: { story: string }) {
  // Stop reading when the user leaves the page.
  useEffect(() => () => stopSpeaking(), []);

  return (
    <article>
      <p>{story}</p>
      <button onClick={() => { stopSpeaking(); speak(story); }}>Read from the start</button>
      <button onClick={stopSpeaking}>Stop</button>
    </article>
  );
}

اقرأ نصًّا طويلًا بصوت عالٍ باللغة والسرعة التي اختارها المستخدم، وأوقفه بزر واحد:

import { useState } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadAloud({ text, lang }: { text: string; lang: string }) {
  const [rate, setRate] = useState(1);
  return (
    <div>
      <input type="range" min={0.5} max={2} step={0.1} value={rate} onChange={(e) => setRate(Number(e.target.value))} />
      <button onClick={() => { stopSpeaking(); speak(text, { lang, rate }); }}>Read aloud</button>
      <button onClick={() => stopSpeaking()}>Stop</button>
    </div>
  );
}

اقطع الصوت عندما يبدأ المستخدم بكتابة رد:

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

export function ReplyBox({ onSend }: { onSend: (text: string) => void }) {
  return <textarea onFocus={() => stopSpeaking()} onBlur={(e) => onSend(e.currentTarget.value)} />;
}

ملاحظات: آمن للاستدعاء عندما لا شيء يُنطق. في تطبيق Android: إيقاف المحرك الأصلي. على الويب: speechSynthesis.cancel().

appmintNative.swipeable appmintNative#

swipeable(el: HTMLElement, opts: SwipeOptions): () => void

مثال

يضيف إيماءات السحب إلى عنصر - صفوف السحب للحذف، والتنقل بين الصفحات، وإغلاق بطاقة.

القيم المُعادة: دالة لإلغاء الاشتراك تستعيد أيضًا قيمة touch-action الأصلية للعنصر. المتطلبات: لا شيء.

الخيارات: onLeft، onRight، onUp، onDown (واحد على الأقل)، threshold (px من الحركة، الافتراضي 56)، edgeGuard (px من حافة الشاشة اليسرى/اليمنى تُتجاهل فيها السحبات الأفقية، الافتراضي 24)، haptics (اهتزاز تحديد عند السحب، الافتراضي true).

اسحب لليسار للحذف، مع زر ظاهر للإجراء نفسه:

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

export function MessageRow({ id, text, onDelete, onUndo }: {
  id: string; text: string; onDelete(id: string): void; onUndo(id: string): void;
}) {
  const ref = useRef<HTMLLIElement>(null);
  useEffect(() => {
    if (!ref.current) return;
    return swipeable(ref.current, {
      onLeft: () => { onDelete(id); toast('Deleted'); },
    });
  }, [id, onDelete]);

  return (
    <li ref={ref} className="flex items-center justify-between p-4">
      <span>{text}</span>
      <button onClick={() => onDelete(id)}>Delete</button>
      {/* swipe is never the only way to an action */}
    </li>
  );
}

تنقّل بين التبويبات بالسحب يسارًا / يمينًا:

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

declare const panel: HTMLElement;
declare let tab: number;
const off = swipeable(panel, { onLeft: () => { tab += 1; }, onRight: () => { tab -= 1; }, threshold: 72 });
// later: off();

أغلق بطاقة بسحبها للأعلى، مع إزالة المعالج عند إزالة المكوّن:

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

export function Banner({ onClose, children }: { onClose: () => void; children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => (ref.current ? swipeable(ref.current, { onUp: onClose, threshold: 40 }) : undefined), [onClose]);
  return <div ref={ref} className="banner">{children}</div>;
}

ملاحظات:

  • مبني على Pointer Events، لذلك يعمل الكود نفسه مع اللمس والقلم وسحب الفأرة.
  • يضبط touch-action لك: pan-y للمعالجات الأفقية (يبقى التمرير العمودي يعمل)، وpan-x للعمودية، وnone لكليهما.
  • يجب أن تكون السحبة واضحة في اتجاه واحد (أكثر بمرة ونصف على محور واحد)؛ والتمريرات المائلة لا تُطلق. الإصبع الثاني يلغي الإيماءة.
  • يوجد حارس الحافة لأن إيماءة الرجوع في نظام Android تملك حواف الشاشة.

appmintNative.systemPalette appmintNative#

systemPalette(): Promise<SystemPalette>

مثال

يقرأ ألوان Material You في الهاتف (المبنية من الخلفية على Android 12+)، فيستطيع لون التطبيق المميّز أن يتبع خلفية المستخدم مثل تطبيقات النظام.

القيمة المُرجعة: Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }> - كل عيّنة تربط درجة ('0'، '10'، '50'، '100' … '900'، '1000') بلون سداسي. { available: false } في أندرويد دون 12 وعلى الويب. المتطلبات: لا شيء.

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

export async function applyWallpaperAccent() {
  const p = await systemPalette();
  if (!p.available || !p.accent1) return;          // keep the app's own palette
  const root = document.documentElement.style;
  root.setProperty('--accent', p.accent1['600']);
  root.setProperty('--accent-soft', p.accent1['100']);
}

استخدم ألوان الخلفية فقط عندما يوفّرها الهاتف، وأعد قراءتها عند عودة التطبيق إلى الشاشة:

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

export function useWallpaperAccent(enabled: boolean) {
  const [accent, setAccent] = useState<string | null>(null);
  useEffect(() => {
    if (!enabled) { setAccent(null); return; }
    const read = () => systemPalette().then((p) => setAccent(p.available ? p.accent1?.['600'] ?? null : null));
    void read();
    document.addEventListener('visibilitychange', read);
    return () => document.removeEventListener('visibilitychange', read);
  }, [enabled]);
  return accent;   // null = no wallpaper palette on this device (Android 11 and older, or the web)
}

اعرض مفتاح "طابق خلفيتي" فقط عند وجود لوحة ألوان، مستخدمًا { available: false } كجواب على "ليس هنا":

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

export function WallpaperToggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) {
  const [ok, setOk] = useState(false);
  useEffect(() => { if (can('systemPalette') !== 'none') systemPalette().then((p) => setOk(p.available)); }, []);
  if (!ok) return null;
  return <label><input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} /> Match my wallpaper</label>;
}

ملاحظات: تحقق من التباين قبل استخدام درجة لون من الخلفية للنصوص أو الأزرار - يجب أن يبلغ --accent 3:1 على خلفيتك. قدّمه كخيار ("مطابقة خلفيتي")، وليس كسمة مفروضة. استدعِه مجددًا عند استئناف التطبيق؛ فقد يكون المستخدم غيّر الخلفية.

appmintNative.TextToSpeech appmintNative#

TextToSpeech — @/lib/appmintNative

مثال

الكائن TextToSpeech بأسلوب Capacitor (speak / stop)، ويعمل بنفس كود speak() و stopSpeaking().

القيمة المُرجعة: speak({ text, lang?, rate?, pitch? }) ← Promise<void> تُحلّ بعد نطق النص (أو بعد أن قطعه stop()) وتُرفض عندما تعذّر نطقه. stop() ← Promise<void>. المتطلبات: لا شيء.

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

async function announce(orderNo: number) {
  await TextToSpeech.speak({ text: `Order ${orderNo} is ready`, lang: 'en-US', rate: 1, pitch: 1 });
  // Here the text is queued; it may still be speaking.
}

async function silence() {
  await TextToSpeech.stop();
}

اقرأ قائمة أسطر واحدًا تلو الآخر؛ يُحلّ كل speak عند وضعه في الطابور، فيشغّلها المحرك بالترتيب:

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

export async function readSteps(steps: string[]) {
  await TextToSpeech.stop();   // drop anything still waiting from before
  for (const [i, s] of steps.entries()) {
    await TextToSpeech.speak({ text: `Step ${i + 1}. ${s}`, lang: 'en-US' });
  }
}

زر قراءة بصوت عالٍ يتوقف عن النطق عند مغادرة الشاشة:

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

export function ArticleReader({ text }: { text: string }) {
  useEffect(() => () => { void TextToSpeech.stop(); }, []);
  return <button onClick={() => void TextToSpeech.speak({ text, rate: 0.9 })}>Listen</button>;
}

ملاحظات: await TextToSpeech.speak(…) هي إشارة النهاية: يمكن للخطوة التالية بعد الكلام أن تتبعها مباشرة. في التطبيق: محرك تحويل النص إلى كلام الأصلي في Android داخل الغلاف؛ وعلى الويب: speechSynthesis.

appmintNative.textZoom appmintNative#

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

مثال

عنصر تحكم في حجم النص داخل التطبيق. في التطبيق يغيّر تكبير نص الصفحة (1 = 100%)؛ وعلى الويب يغيّر حجم الخط الجذري، مما يغيّر مقياس كل تخطيط يعتمد على rem.

القيمة المُعادة: get() → Promise<number>. getPreferred() → Promise<number> - الحجم الذي اختاره المستخدم في إعدادات الهاتف (1 على الويب). set(value) → Promise<void>؛ تُقيَّد القيمة بين 0.5 و3، ويُرفض الطلب لقيمة ليست رقمًا. المتطلبات: لا شيء.

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

async function bigger() { await textZoom.set((await textZoom.get()) + 0.1); }
async function smaller() { await textZoom.set((await textZoom.get()) - 0.1); }
async function reset() { await textZoom.set(await textZoom.getPreferred()); }

تذكّر الاختيار عبر عمليات التشغيل:

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

const saved = Number(localStorage.getItem('textZoom'));
if (saved) await textZoom.set(saved);

export async function setSize(v: number) {
  await textZoom.set(v);
  localStorage.setItem('textZoom', String(v));
}

زر تعديل حجم النص يقرأ القيمة الحالية، ويبقيها ضمن النطاق المسموح، ويعرضها بالنسبة المئوية:

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

export function TextSizeStepper() {
  const [z, setZ] = useState(1);
  useEffect(() => { textZoom.get().then(setZ); }, []);
  const change = async (d: number) => {
    const next = Math.min(3, Math.max(0.5, Math.round((z + d) * 10) / 10));
    await textZoom.set(next);
    setZ(next);
  };
  return (
    <div>
      <button onClick={() => change(-0.1)}>A-</button>
      <span>{Math.round(z * 100)}%</span>
      <button onClick={() => change(0.1)}>A+</button>
    </div>
  );
}

ملاحظات: يتبع التطبيق أصلًا إعداد حجم الخط في الهاتف؛ وهذا تحكم إضافي داخل التطبيق. اختبر التخطيط بالأحجام الكبيرة - الارتفاعات الثابتة في عناصر النص تقطع المحتوى.

appmintNative.toast appmintNative#

toast(message: string, opts?: { duration?: 'short' | 'long' }): void

مثال

يعرض رسالة قصيرة لا تحتاج إلى إجابة - "تم النسخ" أو "تم الحفظ" أو "أُضيف إلى القائمة".

القيمة المُرجعة: لا شيء (أطلق وانسَ). المتطلبات: لا شيء.

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

export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await onSave();
        haptic('success');
        toast('Saved');                              // 'short' ≈ 2 s
      }}
    >
      Save
    </button>
  );
}

الرسالة الأطول تبقى نحو 3.5 ثانية:

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

toast('Your export will be ready in a minute', { duration: 'long' });

الشكل بأسلوب Capacitor Toast.show({ text, duration? }) يفعل الشيء نفسه ويُحلّ Promise<void>:

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

await Toast.show({ text: 'Added to list' });

لرسالة يجب على المستخدم رؤيتها، استخدم حوارًا بدلًا من ذلك:

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

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });

ملاحظات:

  • في تطبيق Android يعرضه مكوّن Capacitor Toast في أسفل الشاشة.
  • في أي مكان آخر هو شريط snackbar صغير يُرسم فوق الصفحة بألوان التطبيق نفسه (--ink على --bg)، فوق الحافة الآمنة، مع role="status" ليعلنه قارئو الشاشة.
  • تظهر واحدة فقط في كل مرة؛ والرسالة الجديدة تحل محل القديمة.
  • إشعار 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.' });

ملاحظات:

  • في تطبيق Android يعرضه مكوّن Capacitor Toast في أسفل الشاشة.
  • في أي مكان آخر هو شريط 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 الخاص بالمسار بدلًا من ذلك.

appmintNative.trackEvent appmintNative#

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

مثال

يبلّغ عن لحظة في تطبيقك لتظهر رسائل المنشئ داخل التطبيق ذات المشغّل "When an event happens" (الاسم نفسه). ينزّل التطبيق حملات المنشئ من مركز إرسال AppMint Push ويعرضها بنفسه.

القيمة المُرجعة: boolean بشكل متزامن: false لاسم غير صالح (أحرف وأرقام و_ . : -، حتى 64)، وعلى الويب، وفي التطبيقات المنشأة بدون AppMint Push. المتطلبات: أندرويد: تفعيل AppMint Push في خطوة التكامل (الخطوة 3) عند الإنشاء.

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

export async function completeCheckout(cart: { total: number }) {
  await saveOrder(cart);
  trackEvent('checkout_done', { total: cart.total });   // props are optional and not stored
}

أبلغ عن تغيير عرض لا يظهره الرابط، كي تراه رسائل "في صفحة":

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

ملاحظات: تغييرات الرابط (تحميل الصفحات وpushState ومسارات #hash) تُرصد أصلًا؛ أبلغ فقط عما لا يغيّر العنوان.

appmintNative.transition appmintNative#

transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>

مثال

يشغّل تغيير شاشة بحركة تشبه الأصلية: push يُدخل الشاشة الجديدة من اليمين، وpop يرجع، وfade يمزج بالتلاشي.

القيمة المُعادة: Promise<void> يتم بعد التغيير وحركته. update هو تغيير حالتك (navigate للموجّه، أو setState) ويُطبَّق دائمًا. المتطلبات: لا شيء.

import { useNavigate } from 'react-router-dom';
import { transition, haptic } from '@/lib/appmintNative';

export function NoteRow({ id, title }: { id: string; title: string }) {
  const navigate = useNavigate();
  return (
    <button
      onClick={() => { haptic('light'); void transition(() => navigate(`/note/${id}`)); }}   // push
    >
      {title}
    </button>
  );
}

export function BackButton() {
  const navigate = useNavigate();
  return <button onClick={() => void transition(() => navigate(-1), { type: 'pop' })}>Back</button>;
}

تلاشٍ بين تبويبين:

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

declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });

استخدم تلاشيًا لتغيير التبويب، في معالج واحد لكل تبويب:

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

export function Tabs({ tab, setTab }: { tab: 'home' | 'stats'; setTab: (t: 'home' | 'stats') => void }) {
  return (
    <nav>
      <button onClick={() => void transition(() => setTab('home'), { type: 'fade' })} aria-current={tab === 'home'}>Home</button>
      <button onClick={() => void transition(() => setTab('stats'), { type: 'fade' })} aria-current={tab === 'stats'}>Stats</button>
    </nav>
  );
}

ملاحظات:

  • يستخدم View Transitions API عندما تتوفر في WebView. وإلا فإنه يطبّق التحديث ويشغّل حركة دخول قصيرة على #root (أو أول عنصر ابن لـ <body>).
  • عندما يطلب المستخدم من النظام تقليل الحركة، يطبّق التحديث فقط - بلا حركة.
  • مدة الحركات نحو 240 ms.

appmintNative.unlock appmintNative#

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

مثال

يطلب من مالك الهاتف إثبات وجوده - بصمة أو وجه أو PIN/رمز مرور الجهاز - ويخبرك إن فعل.

القيمة المُعادة: Promise<boolean> - true عند فتح القفل؛ false عند الإلغاء أو الفشل أو الحظر أو عدم وجود ما يمكنه التحقق على هذا الجهاز. لا يرمي أبدًا. المتطلبات: في التطبيق، Fingerprint في الخطوة 4 (Access) عند الإنشاء. على الويب: المصادِق المدمج في المتصفح (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 الجهاز أو النمط أو كلمة المرور كطريقة دخول ثانية. على الويب، يسجّل الاستدعاء الأول مصادِق منصة المتصفح ويُحتسب كمفتوح. هذا يثبت وجود مالك الجهاز؛ وهو ليس تسجيل دخول - لا تستخدمه أبدًا كحارس وحيد للبيانات الموجودة على خادم. في أندرويد 11 وما بعده تُفتح المطالبة على أي هاتف له قفل شاشة - الهاتف الذي يملك PIN فقط يحصل عليها أيضًا ويُفتح بالـ PIN؛ وفي أندرويد 10 وما قبله يجب حفظ بصمة أولًا، وإلا يُجيب بـ false دون مطالبة.

appmintNative.updates appmintNative#

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

مثال

محدّث من جانب الصفحة لتحديثات حزمة الويب. لا يحتاجه كود الصفحة: في تطبيق AppMint يتم التحديث عن بُعد أصليًا بواسطة تطبيق Android، ويُرجع updates.supported() القيمة false في كل مكان.

القيمة المُعادة: supported() → boolean (حاليًا دائمًا false)؛ download(url, sha256, version, binaryVersion) → Promise<boolean> (false ما دامت غير مدعومة)؛ stage(dir, version, binaryVersion) → Promise<boolean>؛ confirm() → void؛ revertIfUnconfirmed(binaryVersion) → Promise<boolean>؛ current() → { path, version, binaryVersion } | null. المتطلبات: لا شيء لتطبيقك - تُنشر التحديثات من Remote Update في AppMint.

الاستخدام الآمن الوحيد في كود التطبيق هو التحقق، وعدم فعل شيء عندما تكون الميزة متوقفة:

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

if (updates.supported()) {
  // Not reached today: the Android app's own updater owns the whole update lane.
  const staged = await updates.download(
    'https://example.com/bundle-1.2.0.zip',
    'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',  // sha256, required
    '1.2.0',
    '1',
  );
  console.log('update staged:', staged);
}

اعرض أي حزمة تعمل (صف "حول" في الإعدادات):

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

const active = updates.current();          // null = no page-staged bundle (always, today)
const label = active ? `Content ${active.version}` : 'Built-in content';

اعرض إصدار المحتوى الجاري في صف الإعدادات، مع التسمية المضمّنة عندما لا تحتوي الصفحة على حزمة مرحلية:

import { useMemo } from 'react';
import { updates } from '@/lib/appmintNative';

export function AboutRow() {
  const label = useMemo(() => {
    const c = updates.current();
    return c ? `Content ${c.version} (app ${c.binaryVersion})` : 'Built-in content';
  }, []);
  return <p>{label}</p>;
}

ملاحظات:

  • في تطبيق Android يقوم مدير التحديث عن بُعد في الغلاف بتنزيل التحديثات والتحقق منها وتقديمها، ويتراجع عن حزمة يفشل تحميلها. تُقدَّم الصفحة من الغلاف نفسه، لذا لن تظهر أبدًا حزمة مُجهَّزة من كود الصفحة - ولهذا السبب تكون supported() false.
  • على الويب لا يوجد ما يُحدَّث؛ الصفحة هي ما يرسله الخادم.
  • يمكن للتحديثات تغيير HTML وCSS وJavaScript فقط؛ أما الميزة الأصلية الجديدة فتحتاج إلى إنشاء تطبيق جديد.

appmintNative.watchIdle appmintNative#

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

مثال

يخبر التطبيق عندما يصبح المستخدم خاملًا أو يقفل الشاشة، وعندما يعود (can('idle')). ابدأه من نقرة في أول مرة - فطلب الإذن يحتاج إلى نقرة.

القيمة المُعادة: Promise لدالة الإيقاف. يتلقى الاستدعاء { user: 'active' | 'idle', screen: 'locked' | 'unlocked' } عند البدء وعند كل تغيير. يُرفض مع code 'permission-denied' أو 'unavailable'. المتطلبات: لا شيء، ولا إذن Android.

import { useRef } from 'react';
import { can, watchIdle } from '@/lib/appmintNative';

export function AwayStatus({ setAway }: { setAway: (away: boolean) => void }) {
  const stop = useRef<(() => void) | null>(null);
  if (can('idle') === 'none') return null;
  return (
    <button onClick={async () => {
      stop.current?.();
      stop.current = await watchIdle((s) => setAway(s.user === 'idle' || s.screen === 'locked'), { thresholdMs: 5 * 60000 });
    }}>Show me as away when idle</button>
  );
}

اعرض حالة غياب وأوقف المراقبة عند الإزالة؛ ويُبلَّغ عن الإذن المرفوض مع code:

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

export function useIdleWatch() {
  const stop = useRef<(() => void) | null>(null);
  const [state, setState] = useState<'off' | 'active' | 'idle' | 'denied'>('off');
  useEffect(() => () => stop.current?.(), []);
  // Call this from a tap: the first permission request needs a gesture.
  const start = async () => {
    if (can('idle') === 'none') return;
    try {
      stop.current = await watchIdle((s) => setState(s.user === 'idle' || s.screen === 'locked' ? 'idle' : 'active'));
    } catch (e) {
      if ((e as { code?: string }).code === 'permission-denied') setState('denied');
    }
  };
  return { state, start };
}

أوقف مؤقّت الاستطلاع مؤقتًا بينما المستخدم غائب وحدّث عند عودته:

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

export async function pausePollingWhenIdle(refresh: () => void) {
  let timer: ReturnType<typeof setInterval> | undefined = setInterval(refresh, 30000);
  const stop = await watchIdle((s) => {
    const away = s.user === 'idle' || s.screen === 'locked';
    if (away && timer) { clearInterval(timer); timer = undefined; }
    if (!away && !timer) { refresh(); timer = setInterval(refresh, 30000); }
  });
  return () => { stop(); if (timer) clearInterval(timer); };
}

ملاحظات: الحد الأدنى للعتبة دقيقة واحدة (الحد الأدنى في المواصفة).

appmintNative.widget appmintNative#

widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()

مثال

يملأ ودجت الشاشة الرئيسية للتطبيق - بطاقة واحدة بعنوان وقيمة كبيرة وتعليق وحتى اثني عشر سطرًا (لكل منها لون وعلامة ومسار اختياريان)، ونقرة تفتح التطبيق على مسار.

القيمة المُعادة: set(card) → Promise<boolean> (true عند الحفظ)؛ clear() → Promise<void>؛ placedCount() → Promise<number> (عدد النسخ التي وضعها المستخدم)؛ requestPin() → Promise<boolean> (true عندما فُتح مسار "إضافة ودجت" في المشغّل). لا يُرفض أي منها. المتطلبات: لا شيء في تطبيق Android (الودجت دائمًا جزء من النسخة المبنية).

حدّث البطاقة بعد كل تغيير يرغب المستخدم في رؤيته على الشاشة الرئيسية:

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

export async function updateTodayWidget(tasks: { title: string; done: boolean }[]) {
  if (can('widget') === 'none') return;          // on the web
  const open = tasks.filter((t) => !t.done);
  await widget.set({
    title: 'Today',
    value: `${open.length} left`,
    lines: open.slice(0, 6).map((t) => '• ' + t.title),
    route: '/today',                              // a tap opens the app here
  });
}

أجندة تملأ ودجت معدَّل الحجم، بخلفية شفافة وسطر متأخر بالأحمر:

import { widget, type WidgetLine } from '@/lib/appmintNative';

export async function updateAgendaWidget(events: { day: string; time: string; title: string; overdue: boolean }[]) {
  const lines: WidgetLine[] = events.slice(0, 12).map((e) => e.overdue
    ? { text: `Overdue: ${e.title}`, color: '#FF5252', marker: '#FF5252', bold: true, route: `/day/${e.day}` }
    : { text: `${e.time} ${e.title}`, marker: '#29B6F6', route: `/day/${e.day}` });
  await widget.set({
    title: 'Agenda', value: 'Today', caption: `${events.length} events`,
    bg: 'rgba(20, 38, 74, 0.6)', textSize: 'auto', lines, route: '/agenda',
  });
}

زر "إضافة إلى الشاشة الرئيسية":

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

export function AddWidgetButton() {
  if (can('widget') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (!(await widget.requestPin())) toast('Long-press your home screen, tap Widgets, and pick this app');
      }}
    >
      Add widget to home screen
    </button>
  );
}

أفرغ البطاقة عندما يسجّل المستخدم خروجه، واعرض عدد النسخ الموجودة على الشاشة الرئيسية:

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

export function WidgetStatus({ signedIn }: { signedIn: boolean }) {
  const [placed, setPlaced] = useState(0);
  useEffect(() => {
    if (can('widget') === 'none') return;
    if (!signedIn) void widget.clear();
    widget.placedCount().then(setPlaced);
  }, [signedIn]);
  return <p>{placed === 0 ? 'Not on your home screen yet' : `On your home screen (${placed})`}</p>;
}

ملاحظات:

  • تُحفظ البطاقة على الجهاز، وتبقى بعد إعادة التشغيل، وتُحدَّث كل نسخة موضوعة. تعني placedCount() التي تساوي 0 أنه لا توجد نسخة على الشاشة الرئيسية بعد.
  • حقول البطاقة: title (فارغ = اسم التطبيق)، value (يظهر كبيرًا في accent، ويُخفى عند الفراغ)، caption (نص صغير تحت القيمة)، lines (حتى 12؛ ستة تظهر دائمًا، والمزيد عند ارتفاع الودجت بما يكفي؛ والسطر نص أو { text, color?, marker?, bold?, wrap?, route? })، route (الافتراضي /)، textSize ('auto' يكبّر النص مع الودجت ما دامت كل الأسطر تتسع، مع لف الأسطر الطويلة إلى صفين؛ 'small' | 'normal' | 'large' | 'xl' أو رقم 0.6-3)، align ('start' | 'center' | 'end')، wrap (الأسطر الطويلة تأخذ صفين)، وbg / ink / accent. الألوان الافتراضية هي رموز CSS --bg و--ink و--accent؛ ويجب أن تكون #RRGGBB أو #AARRGGBB (ألفا أولًا: #9914264A معتم بنسبة 60%) أو #RGB أو rgb()/rgba() أو اسم لون - وقيمة hsl()/oklch() تعود إلى الألوان الافتراضية. يُرجع set() القيمة false لقيمة textSize/align غير معروفة أو لون سطر غير صالح.
  • تفتح النقرة التطبيق بـ appmint-shortcut://<package><route>؛ اقرأه بواسطة launchUrl() / onDeepLink().
  • requestPin() يحتاج Android 8 أو أحدث ومشغّلًا (launcher) يدعمه. يعرض clear() بطاقة "لا شيء لعرضه بعد" الفارغة.
  • لتطبيق Android فقط: can('widget') هي 'none' على الويب، حيث تُرجع set() وrequestPin() false.

يتم إنشاؤه من بيئة تشغيل التطبيق وملفات الأمثلة الخاصة به عند كل بناء للمستندات. اقرأه بصيغة Markdown · جميع العائلات.

تمت المراجعة مقابل الجسر المُرفق بتاريخ 2026-10-22.