أنشئ APK مجانًا

واجهة JavaScript bridge API

الإشعارات على الجهاز

النشر والجدولة دون خادم، ومنحة POST_NOTIFICATIONS، وexact alarms، والأصوات المخصصة.

AppMint.enableNotifications دالة مساعدة (helper)#

AppMint.enableNotifications()

مثال

"Turn on notifications" باستدعاء واحد، للمستخدمين الذين رفضوا أو تخطوا نافذة إشعارات Android: نافذة الإذن ما دام Android يستطيع عرضها، وإلا إعدادات إشعارات هذا التطبيق - والحالة الجديدة عندما يعود المستخدم.

القيمة المُعادة: AppMint.enableNotifications() - Promise يُحَلّ دائمًا بـ {state, changed, openedSettings}؛ وstate هي 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() - Promise بتلك الحالة. AppMint.openNotificationSettings() - Promise بـ true عندما فُتحت شاشة الإعدادات. المتطلبات: استدعِ enableNotifications() من زر نقره المستخدم؛ والإشعارات تحتاج Notifications (أو Push / Reminders) في الإنشاء.

// A banner that only shows while notifications are off, and turns them on in one tap.
async function setupNotifBanner() {
  if (!(window.AppMint && AppMint.enableNotifications)) return;   // a browser: no banner
  var banner = document.getElementById('notif-banner');
  var state = await AppMint.getNotificationState();
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  document.getElementById('notif-turn-on').onclick = async function () {
    var r = await AppMint.enableNotifications();
    banner.hidden = r.state === 'granted';
    if (r.state === 'blocked') showToast('Switch on "Allow notifications" to get updates.');
  };
}
setupNotifBanner();

رابط عادي "Notification settings" في صفحة إعداداتك:

document.getElementById('notif-settings').onclick = async function () {
  var opened = window.AppMint && AppMint.openNotificationSettings && await AppMint.openNotificationSettings();
  if (!opened) showToast('Open Settings > Apps > this app > Notifications.');
};

ملاحظات: زر في رسالة push أو رسالة داخل التطبيق رابطه appmint://notifications/enable يشغّل التدفق نفسه دون أي شيفرة في الصفحة (وتسمع الصفحة appmint:notifications). يتحدث جمهور push في التطبيق ("Notifications on / off") بمجرد أن يشغّلها المستخدم.

AppMint.getNotificationState دالة مساعدة (helper)#

window.AppMint.getNotificationState

مثال

موثَّق مع AppMint.enableNotifications - والمثال هناك يعرض هذا أيضًا.

"Turn on notifications" باستدعاء واحد، للمستخدمين الذين رفضوا أو تخطوا نافذة إشعارات Android: نافذة الإذن ما دام Android يستطيع عرضها، وإلا إعدادات إشعارات هذا التطبيق - والحالة الجديدة عندما يعود المستخدم.

القيمة المُعادة: AppMint.enableNotifications() - Promise يُحَلّ دائمًا بـ {state, changed, openedSettings}؛ وstate هي 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() - Promise بتلك الحالة. AppMint.openNotificationSettings() - Promise بـ true عندما فُتحت شاشة الإعدادات. المتطلبات: استدعِ enableNotifications() من زر نقره المستخدم؛ والإشعارات تحتاج Notifications (أو Push / Reminders) في الإنشاء.

// A banner that only shows while notifications are off, and turns them on in one tap.
async function setupNotifBanner() {
  if (!(window.AppMint && AppMint.enableNotifications)) return;   // a browser: no banner
  var banner = document.getElementById('notif-banner');
  var state = await AppMint.getNotificationState();
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  document.getElementById('notif-turn-on').onclick = async function () {
    var r = await AppMint.enableNotifications();
    banner.hidden = r.state === 'granted';
    if (r.state === 'blocked') showToast('Switch on "Allow notifications" to get updates.');
  };
}
setupNotifBanner();

رابط عادي "Notification settings" في صفحة إعداداتك:

document.getElementById('notif-settings').onclick = async function () {
  var opened = window.AppMint && AppMint.openNotificationSettings && await AppMint.openNotificationSettings();
  if (!opened) showToast('Open Settings > Apps > this app > Notifications.');
};

ملاحظات: زر في رسالة push أو رسالة داخل التطبيق رابطه appmint://notifications/enable يشغّل التدفق نفسه دون أي شيفرة في الصفحة (وتسمع الصفحة appmint:notifications). يتحدث جمهور push في التطبيق ("Notifications on / off") بمجرد أن يشغّلها المستخدم.

AppMint.openNotificationSettings دالة مساعدة (helper)#

window.AppMint.openNotificationSettings

مثال

موثَّق مع AppMint.enableNotifications - والمثال هناك يعرض هذا أيضًا.

"Turn on notifications" باستدعاء واحد، للمستخدمين الذين رفضوا أو تخطوا نافذة إشعارات Android: نافذة الإذن ما دام Android يستطيع عرضها، وإلا إعدادات إشعارات هذا التطبيق - والحالة الجديدة عندما يعود المستخدم.

القيمة المُعادة: AppMint.enableNotifications() - Promise يُحَلّ دائمًا بـ {state, changed, openedSettings}؛ وstate هي 'granted' | 'prompt' | 'blocked' | 'unavailable'. AppMint.getNotificationState() - Promise بتلك الحالة. AppMint.openNotificationSettings() - Promise بـ true عندما فُتحت شاشة الإعدادات. المتطلبات: استدعِ enableNotifications() من زر نقره المستخدم؛ والإشعارات تحتاج Notifications (أو Push / Reminders) في الإنشاء.

// A banner that only shows while notifications are off, and turns them on in one tap.
async function setupNotifBanner() {
  if (!(window.AppMint && AppMint.enableNotifications)) return;   // a browser: no banner
  var banner = document.getElementById('notif-banner');
  var state = await AppMint.getNotificationState();
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  document.getElementById('notif-turn-on').onclick = async function () {
    var r = await AppMint.enableNotifications();
    banner.hidden = r.state === 'granted';
    if (r.state === 'blocked') showToast('Switch on "Allow notifications" to get updates.');
  };
}
setupNotifBanner();

رابط عادي "Notification settings" في صفحة إعداداتك:

document.getElementById('notif-settings').onclick = async function () {
  var opened = window.AppMint && AppMint.openNotificationSettings && await AppMint.openNotificationSettings();
  if (!opened) showToast('Open Settings > Apps > this app > Notifications.');
};

ملاحظات: زر في رسالة push أو رسالة داخل التطبيق رابطه appmint://notifications/enable يشغّل التدفق نفسه دون أي شيفرة في الصفحة (وتسمع الصفحة appmint:notifications). يتحدث جمهور push في التطبيق ("Notifications on / off") بمجرد أن يشغّلها المستخدم.

navigator.setAppBadge معيار الويب#

navigator.setAppBadge(count) / clearAppBadge()

مثال

Badging API القياسي: رقم على أيقونة launcher للتطبيق، مثل عدد غير المقروء.

القيمة المُعادة: Promise يُحَلّ عند ضبط الشارة؛ TypeError لعدّاد سالب أو غير رقمي. المتطلبات: لا شيء لتفعيله. وهو موجود فقط على الهواتف التي تعرض launcher فيها عدّادات (Samsung وHuawei وXiaomi وOPPO وSony …)؛ وعلى launchers بنمط Pixel تكون 'setAppBadge' in navigator false.

function updateBadge(unread) {
  if (!('setAppBadge' in navigator)) return;      // this launcher cannot show a count
  if (unread > 0) navigator.setAppBadge(unread);
  else navigator.clearAppBadge();
}

updateBadge(messages.filter((m) => !m.read).length);

امسح الشارة كلما كان المستخدم ينظر إلى التطبيق، وتعامل مع تحديث مرفوض:

function setUnread(count) {
  if (!('setAppBadge' in navigator)) return;
  navigator.setAppBadge(count).catch(function (err) {
    console.warn('badge not updated:', err.name);   // NotAllowedError or TypeError
  });
}

document.addEventListener('visibilitychange', function () {
  if (!document.hidden && 'clearAppBadge' in navigator) {
    navigator.clearAppBadge().catch(function () {});
  }
});

اعرض علم "something new" عاديًا عندما تجد المزامنة تغييرات لكن بلا عدد دقيق:

async function afterSync(result) {
  if (!('setAppBadge' in navigator)) return;
  try {
    if (result.newCount > 0) await navigator.setAppBadge(result.newCount);
    else if (result.hasChanges) await navigator.setAppBadge();   // flag badge, shown as 1
    else await navigator.clearAppBadge();
  } catch (e) {
    console.warn('badge update failed', e);
  }
}

ملاحظات: setAppBadge() بدون رقم هو شارة "flag" في المعيار، وتظهر كـ 1 على launcher بعدّاد. يبقى العدد بعد إعادة تشغيل التطبيق. المخزن نفسه يدعم badge.set() في @/lib/appmintNative، فيتفقان دائمًا.

Notification معيار الويب#

new Notification(title, options) / Notification.requestPermission()

مثال

تعمل واجهة Web Notifications القياسية داخل التطبيق: يعرض new Notification() إشعار Android حقيقيًا، ويزيله close()، ويعرض Notification.requestPermission() نافذة إذن Android وينتظر إجابة المستخدم.

القيمة المُعادة: Notification.permission هي 'granted' أو 'denied' أو 'default'، تُقرأ مباشرة من الهاتف (يظهر التغيير في الإعدادات دون إعادة تحميل). يعيد requestPermission() Promise بـ 'granted' أو 'denied' (وصيغة callback تعمل أيضًا). الإشعار الجديد يُطلِق show عند نشره، أو error عند غياب الإذن. المتطلبات: فعّل Notifications في خطوة Access (الخطوة 4) عند الإنشاء.

async function notifyWhenSaved() {
  if (!('Notification' in window)) return;
  let perm = Notification.permission;
  if (perm === 'default') perm = await Notification.requestPermission();
  if (perm !== 'granted') {
    showMessage('Turn on notifications for this app in Settings to get alerts.');
    return;
  }
  const n = new Notification('Saved', {
    body: 'Your note was saved',
    icon: 'https://example.com/avatar.png',   // shown as the large icon
    image: 'https://example.com/note.jpg',    // big picture
    tag: 'note'                               // same tag replaces the old one
  });
  n.onshow = function () { console.log('posted'); };
  n.onerror = function () { console.log('not posted'); };
  setTimeout(function () { n.close(); }, 60000);   // removes it from the shade
}

document.getElementById('save').addEventListener('click', notifyWhenSaved);

صيغة callback (للشيفرة الأقدم):

Notification.requestPermission(function (result) {
  if (result === 'granted') new Notification('Welcome!', { body: 'Alerts are on', silent: true });
});

أسلوب Service worker (شيفرة PWA) ينشر عبر التطبيق أيضًا، لكن فقط حيث لا يملك WebView نفسه navigator.serviceWorker (مثل صفحات file:// أو http://):

if ('serviceWorker' in navigator && Notification.permission === 'granted') {
  navigator.serviceWorker.ready.then(function (reg) {
    return reg.showNotification('Backup done', { body: '12 files', tag: 'backup' });
  });
}

النقرة تطلق click على الإشعار - يأتي التطبيق إلى الواجهة وتتلقاه الصفحة. إذا كان التطبيق مغلقًا (بدأته النقرة)، يكون الكائن قد زال، فتتلقى النافذة حدث notificationclick بدلًا من ذلك، بشكل مماثل لـ service worker - event.notification.tag / title / body / data:

const n = new Notification('Order shipped', { body: '#7', tag: 'order-7', data: { orderId: 7 } });
n.onclick = () => openOrder(7);                         // the app was running
window.addEventListener('notificationclick', (e) => {   // the tap started the app
  if (e.notification.data && e.notification.data.orderId) openOrder(e.notification.data.orderId);
});

ملاحظات: الخيارات المستخدمة: body وicon وimage وtag وsilent وdata (JSON، أقل من 16 KB). تصل النقرة إلى الصفحة عبر شاشة البداية وشاشة PIN (الحدث خلفها: appmint:notification-click). في Android 12 والأقدم لا يوجد إذن وقت التشغيل، فتكون القيمة 'granted' ما لم يوقف المستخدم الإشعارات. Web Push (pushManager.subscribe) غير مدعوم؛ استخدم AppMint Push. للأزرار أو التقدم أو الإشعار الثابت استخدم WebToApk.notify(...). الشيفرة نفسها تعمل في متصفح عادي.

__badgeSupported الجسر (bridge)#

window.WebToApk.__badgeSupported(): Boolean

هل تستطيع هذه launcher عرض عدّاد شارة (navigator.setAppBadge موجود فقط حينها).

مثال

بنية نقل داخلية خلف وجود navigator.setAppBadge. تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - فقد تتغير طرق __ دون إشعار.

القيمة المُعادة: true عندما تعرض launcher هذا الهاتف عدّادات الشارات. المتطلبات: لا شيء لتفعيله.

استخدم الواجهة العامة:

if ('setAppBadge' in navigator) navigator.setAppBadge(unreadCount);

اعرض إعداد "badge on app icon" فقط على الهواتف التي يستطيع launcher فيها عرض عدّاد:

function renderBadgeSetting() {
  const row = document.getElementById('badge-setting');
  // setAppBadge exists only when this launcher shows counts
  row.hidden = !('setAppBadge' in navigator);
}
renderBadgeSetting();

امسح العدّاد عندما يفتح المستخدم صندوق الوارد، على أي launcher يملك واحدًا:

function openInbox() {
  showInbox();
  if ('clearAppBadge' in navigator) {
    navigator.clearAppBadge().catch(function (e) {
      console.warn('badge not cleared', e.name);   // NotAllowedError
    });
  }
}

ملاحظات: يُثبَّت Badging API فقط حيث تستطيع launcher عرض عدّاد (Samsung وHuawei وXiaomi وOPPO وSony …)؛ وهو غير موجود على launchers بنمط Pixel، كما في متصفح بلا badging.

__closeNotification الجسر (bridge)#

window.WebToApk.__closeNotification(tag: String): Boolean

Notification.close(): يزيل إشعارًا نشرته الصفحة، بوسمه.

مثال

بنية داخلية خلف Notification.prototype.close() - تستخدم الصفحات new Notification(...) وتستدعي close() عليه. يزيل إشعارًا من الشريط بوسمه.

القيمة المُعادة: true عندما تم طلب الإزالة (وأيضًا عندما لا يوجد ما يحمل ذلك الوسم)؛ false عند خطأ داخلي. المتطلبات: لا شيء.

استخدم الواجهة العامة:

let uploadNote = null;

function showUploading() {
  if (!('Notification' in window) || Notification.permission !== 'granted') return;
  uploadNote = new Notification('Uploading…', { body: 'Keep the app open', tag: 'upload', silent: true });
  uploadNote.onclose = function () { uploadNote = null; };
}

function uploadFinished() {
  if (uploadNote) uploadNote.close();   // removes it from the notification shade
}

افتح المحادثة الصحيحة عند النقر على الإشعار، ثم أزله من الشريط:

function notifyNewMessage(msg) {
  if (!('Notification' in window) || Notification.permission !== 'granted') return;
  const n = new Notification(msg.from, {
    body: msg.text,
    tag: 'chat-' + msg.threadId,          // one notification per chat
    data: { threadId: msg.threadId }
  });
  n.onclick = function () {
    openThread(n.data.threadId);
    n.close();
  };
}

اعرض تأكيدًا قصيرًا يزيل نفسه بعد خمس ثوانٍ:

function confirmCopied() {
  if (!('Notification' in window) || Notification.permission !== 'granted') return;
  const n = new Notification('Link copied', { body: 'Paste it anywhere', tag: 'copied', silent: true });
  setTimeout(function () { n.close(); }, 5000);
}

ملاحظات: لا تستدعِ WebToApk.__closeNotification مباشرة؛ فقد تتغير طرق __ دون إشعار. يطلق close() حدث close الخاص بالإشعار.

__setAppBadge الجسر (bridge)#

window.WebToApk.__setAppBadge(contents: Long): Boolean

navigator.setAppBadge(n): القيمة n >= 0 عدّاد (0 يمسح)، و-1 شارة العلم في المعيار.

مثال

بنية نقل داخلية خلف navigator.setAppBadge(count) / clearAppBadge(). تستخدم الصفحات الواجهة القياسية؛ لا تستدعِ هذا مباشرة - فقد تتغير طرق __ دون إشعار.

القيمة المُعادة: true عندما طُبّق العدّاد. المتطلبات: لا شيء لتفعيله.

استخدم الواجهة العامة:

if ('setAppBadge' in navigator) navigator.setAppBadge(unreadCount);

أبقِ عدّاد الأيقونة متوافقًا مع عدد غير المقروء، وامسحه عند الصفر:

async function syncBadge(unread) {
  if (!('setAppBadge' in navigator)) return;
  try {
    if (unread > 0) await navigator.setAppBadge(unread);
    else await navigator.clearAppBadge();
  } catch (e) {
    console.warn('badge not applied', e.name);   // NotAllowedError when the launcher refused it
  }
}
syncBadge(messages.filter(function (m) { return !m.read; }).length);

ضع شارة علم (بدون رقم) على الأيقونة أثناء وجود عمل غير مزامَن:

function markPendingSync(hasPending) {
  if (!('setAppBadge' in navigator)) return;
  const p = hasPending
    ? navigator.setAppBadge()      // no argument = flag badge; a count launcher shows 1
    : navigator.clearAppBadge();
  p.catch(function () {});
}

ملاحظات: -1 هي شارة العلم في المعيار (بدون رقم)، وتعرضها launcher العدّاد كـ 1.

cancelAllNotifications الجسر (bridge)#

window.WebToApk.cancelAllNotifications(): Boolean

قراءة منحة POST_NOTIFICATIONS وطلبها، وإدارة الإشعارات التي جدولتها الصفحة بالفعل. النشر والجدولة موثّقان في طرقهما الخاصة.

موصوف عبر مجموعته، Notification permission, cancelling and listing، وليس بمفرده.

مثال

يلغي كل تذكير جدولهُ هذا التطبيق.

القيمة المُعادة: true عند الانتهاء؛ false فقط عند خطأ داخلي. المتطلبات: لا شيء إضافي.

document.getElementById('reset-reminders').addEventListener('click', function () {
  if (!(window.WebToApk && window.WebToApk.cancelAllNotifications)) return;
  if (!confirm('Remove all your reminders?')) return;
  const ok = WebToApk.cancelAllNotifications();
  showMessage(ok ? 'All reminders removed' : 'Could not remove reminders');
});

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

async function signOut() {
  if (window.WebToApk && window.WebToApk.cancelAllNotifications) {
    WebToApk.cancelAllNotifications();
  }
  if ('clearAppBadge' in navigator) navigator.clearAppBadge().catch(function () {});
  await auth.signOut();
  showLoginScreen();
}

أعد بناء الجدول اليومي كله من الأوقات المحفوظة (ألغِ الكل، ثم أضف كل واحد):

function applyMedicineTimes(times) {        // e.g. ['08:00', '20:00']
  const b = window.WebToApk;
  if (!(b && b.cancelAllNotifications)) return;
  b.cancelAllNotifications();
  times.forEach(function (hhmm, i) {
    const parts = hhmm.split(':');
    const at = new Date();
    at.setHours(Number(parts[0]), Number(parts[1]), 0, 0);
    if (at.getTime() <= Date.now()) at.setDate(at.getDate() + 1);   // next occurrence
    b.scheduleNotification('med-' + i, 'Medicine', 'Time for your ' + hhmm + ' dose',
      String(at.getTime()), 'daily');
  });
}

ملاحظات: يمسح القائمة المجدولة، فلا يُطلَق شيء لاحقًا ولا يُضبط شيء من جديد بعد إعادة التشغيل. الإشعارات المعروضة في الشريط تبقى هناك.

cancelNotification الجسر (bridge)#

window.WebToApk.cancelNotification(id: String): Boolean

قراءة منحة POST_NOTIFICATIONS وطلبها، وإدارة الإشعارات التي جدولتها الصفحة بالفعل. النشر والجدولة موثّقان في طرقهما الخاصة.

موصوف عبر مجموعته، Notification permission, cancelling and listing، وليس بمفرده.

مثال

يلغي تذكيرًا مجدولًا واحدًا بمعرّفه، فلا يُطلَق مرة أخرى.

القيمة المُعادة: true (وأيضًا عندما لا يحمل أي تذكير ذلك المعرّف)؛ false فقط عند خطأ داخلي. المتطلبات: لا شيء إضافي (التذكيرات تحتاج ⏰ Reminders وقت الإنشاء).

function stopWaterReminder() {
  if (!(window.WebToApk && window.WebToApk.cancelNotification)) return;
  WebToApk.cancelNotification('water');   // the id you gave scheduleNotification / scheduleNotificationEx

  // Check it is gone:
  const left = JSON.parse(WebToApk.getScheduledNotifications() || '[]');
  const stillThere = left.some(function (r) { return r.id === 'water'; });
  console.log('water reminder still scheduled?', stillThere);   // false
}

مفتاح تذكير في الإعدادات يجدول أو يلغي بالمعرّف نفسه:

document.getElementById('water-toggle').addEventListener('change', function (e) {
  const b = window.WebToApk;
  if (!(b && b.cancelNotification && b.scheduleNotification)) return;
  if (e.target.checked) {
    b.scheduleNotification('water', 'Drink water', 'Time for a glass of water',
      String(Date.now() + 60 * 60 * 1000), 'hourly');
  } else {
    b.cancelNotification('water');
  }
});

عند حذف مهمة، ألغِ كل تذكير ينتمي إليها:

function cancelTaskReminders(taskId) {
  const b = window.WebToApk;
  if (!(b && b.cancelNotification)) return;
  let list = [];
  try { list = JSON.parse(b.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  list
    .filter(function (r) { return r.id.indexOf('task-' + taskId + '-') === 0; })
    .forEach(function (r) { b.cancelNotification(r.id); });
}

ملاحظات: هذا يلغي تذكيرًا لم يُطلَق بعد (لمرة واحدة أو متكررًا). ولا يزيل إشعارًا يظهر بالفعل في الشريط، ولا يؤثر في الإشعارات المنشورة بـ notify() / showNotification().

canOpenAppAtTime الجسر (bridge)#

window.WebToApk.canOpenAppAtTime(): String

هل يجوز لإشعار مجدول أن يضع التطبيق نفسه على الشاشة (`openApp`). "granted" - سيظهر تنبيه بملء الشاشة، فوق شاشة القفل. "denied" - منعه Android 14+؛ استدعِ requestOpenAppPermission(). لا توجد إجابة ثالثة يطلق فيها التطبيق نفسه صامتًا في الخلفية: يمنع Android بدء الأنشطة من الخلفية منذ Android 10، والـ full-screen intent هو كل ما تبقى.

مثال

يخبرك إن كان يجوز لتذكير بـ openApp: true أن يضع التطبيق نفسه على الشاشة (تنبيه بملء الشاشة، وأيضًا فوق شاشة القفل).

القيمة المُعادة: نص، بشكل متزامن: 'granted' أو 'denied'. دون Android 14 تكون دائمًا 'granted'. المتطلبات: فعّل ⏰ Reminders في خطوة Access (الخطوة 4) عند الإنشاء.

function prepareAlarm() {
  const b = window.WebToApk;
  if (!(b && b.canOpenAppAtTime)) return;
  if (b.canOpenAppAtTime() === 'denied') {
    showMessage('To open the app at alarm time, allow "full-screen notifications" for this app.');
    b.requestOpenAppPermission();
    return;
  }
  const t = new Date();
  t.setDate(t.getDate() + 1);
  t.setHours(7, 0, 0, 0);                    // tomorrow 07:00
  b.scheduleNotificationEx('wake-up', JSON.stringify({
    at: t.getTime(), repeat: 'daily',
    title: 'Wake up', body: 'Good morning!',
    openApp: true
  }));
}

window.addEventListener('appmint:reminder', function (e) {
  // e.detail.overLockScreen is true when the app was shown as a full-screen alert
  if (e.detail.overLockScreen) showBigAlarmScreen(e.detail.id);
});

شاشة إعدادات تعرض مفتاحي المنبّه مع حالتهما الحالية:

function renderAlarmStatus() {
  const b = window.WebToApk;
  if (!(b && b.canOpenAppAtTime && b.getAlarmPrecision)) return;
  const openApp = b.canOpenAppAtTime();       // 'granted' | 'denied'
  const precision = b.getAlarmPrecision();    // 'exact' | 'inexact' | 'unavailable'
  document.getElementById('open-app-state').textContent =
    openApp === 'granted' ? 'Opens over the lock screen' : 'Notification only';
  document.getElementById('open-app-fix').hidden = openApp === 'granted';
  document.getElementById('timing-state').textContent =
    precision === 'exact' ? 'On the minute' : 'May be late';
}

renderAlarmStatus();
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) renderAlarmStatus();  // back from Settings
});

حذّر عندما تكون المنبّهات المحفوظة مضبوطة لفتح التطبيق لكن Android لا يسمح بذلك الآن:

function checkSavedAlarms() {
  const b = window.WebToApk;
  if (!(b && b.canOpenAppAtTime && b.getScheduledNotifications)) return;
  if (b.canOpenAppAtTime() === 'granted') return;
  // [{ id, title, body, trigger, repeat, options }] - options is the JSON string you scheduled
  const items = JSON.parse(b.getScheduledNotifications());
  const affected = items.filter(function (r) {
    try { return JSON.parse(r.options || '{}').openApp === true; } catch (e) { return false; }
  });
  if (affected.length) {
    showMessage(affected.length + ' alarm(s) will only show a notification. Tap "Allow" to let them open the app.');
  }
}

ملاحظات: عندما تكون القيمة 'denied'، يظل تذكير openApp ينشر إشعارًا منبثقًا، لكن التطبيق لا يفتح من تلقاء نفسه. لا يسمح Android بأي طريقة أخرى لتطبيق ليبدأ نفسه في الخلفية.

clearNotificationSounds الجسر (bridge)#

window.WebToApk.clearNotificationSounds(): Int

يُسقط ملفات الصوت المنسوخة ويعيد عدد ما حُذف. يُنسخ الملف مرة أخرى عندما يستخدم إشعار (أو تذكير مجدول، عند إطلاقه) ذلك الصوت لاحقًا - بما في ذلك على قناة موجودة، تظل تشير إلى اسم الملف نفسه (NotificationBridge.restoreBundleSound).

مثال

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

القيمة المُعادة: رقم، بشكل متزامن: عدد الملفات المحذوفة. المتطلبات: لا شيء.

امسح، ثم أعد الأصوات التي ما زلت تستخدمها:

document.getElementById('reset-sounds').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.clearNotificationSounds)) return;
  const removed = b.clearNotificationSounds();

  // Sounds still in use must be copied again, or they play the default sound:
  ['/audio/alarm.mp3', '/audio/chime.mp3'].forEach(function (path) {
    if (!b.prepareNotificationSound(path)) console.warn('missing sound', path);
  });
  showMessage(removed + ' sound file(s) refreshed');
});

زر في شاشة التخزين يحرّر ملفات الصوت المنسوخة ويقول كم منها أُزيل:

document.getElementById('free-space').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.clearNotificationSounds)) return;
  const removed = b.clearNotificationSounds();
  showMessage(removed === 0 ? 'No sound files to remove.' : 'Removed ' + removed + ' sound file(s).');
});

عند تسجيل الخروج، ألغِ كل تذكير واحذف نسخ الأصوات التي كانت تخص هذا المستخدم:

function signOutAndReset() {
  const b = window.WebToApk;
  if (b && b.cancelAllNotifications) b.cancelAllNotifications();
  if (b && b.clearNotificationSounds) b.clearNotificationSounds();
  localStorage.removeItem('alarmSound');
  goToLoginScreen();
}

ملاحظات: نادرًا ما تحتاج هذا؛ النسخ صغيرة. يُنسخ الصوت مرة أخرى في المرة التالية التي يستخدمه فيها أي شيء - notify()، أو تذكير عند إطلاقه، أو scheduleNotificationEx، أو prepareNotificationSound(path) - بما في ذلك على قناة إشعارات موجودة، فيستمر الصوت المخصص بالعمل.

enableNotifications الجسر (bridge)#

window.WebToApk.enableNotifications(callbackId: String)

يعيد المستخدم إلى الإشعارات: نافذة النظام ما دام يمكن عرضها، وإلا فإعدادات إشعارات التطبيق. يُحَلّ `window.__appmintReplies[callbackId]` بـ {state, changed, openedSettings} (AppMint.enableNotifications() هو غلاف الـ promise).

مثال

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

القيمة المُعادة: لا شيء مباشرة. تُسلَّم الإجابة {state, changed, openedSettings} إلى window.__appmintReplies[callbackId]؛ وstate هي 'granted' | 'prompt' | 'blocked' | 'unavailable'، وchanged هي true عندما تختلف عما قبل الاستدعاء، وopenedSettings هي true عندما استُخدمت شاشة الإعدادات. وإذا كانت 'granted' (أو 'unavailable') فالإجابة فورية. المتطلبات: استدعه من زر نقره المستخدم؛ والإشعارات تحتاج Notifications (أو Push / Reminders) في الإنشاء.

يُفضَّل غلاف الـ promise AppMint.enableNotifications(). الاستدعاء الخام، للشيفرة التي تدير ردودها بنفسها:

function enableNotificationsRaw() {
  return new Promise(function (resolve) {
    if (!(window.WebToApk && WebToApk.enableNotifications)) return resolve({ state: 'unavailable', changed: false, openedSettings: false });
    var replies = window.__appmintReplies = window.__appmintReplies || {};
    var id = 'my_notif_' + Date.now();
    replies[id] = resolve;
    WebToApk.enableNotifications(id);
  });
}

document.getElementById('turn-on-btn').onclick = async function () {
  var r = await enableNotificationsRaw();
  if (r.state === 'granted') document.getElementById('notif-banner').hidden = true;
};

كل تدفق منتهٍ يُعلَن عنه أيضًا، بما في ذلك تدفق بدأه زر push (appmint://notifications/enable):

window.addEventListener('appmint:notifications', function (e) {
  // e.detail = { state, changed, openedSettings }
  if (e.detail.state === 'granted') document.getElementById('notif-banner').hidden = true;
});

ملاحظات: زر في رسالة push أو رسالة داخل التطبيق رابطه appmint://notifications/enable يشغّل هذا التدفق نفسه؛ وappmint://notifications/settings يفتح شاشة الإعدادات. يرى جمهور push في التطبيق الحالة الجديدة فورًا.

getAlarmPrecision الجسر (bridge)#

window.WebToApk.getAlarmPrecision(): String

"exact" - التذكيرات تُطلَق في الدقيقة. "inexact" - أوقف المستخدم (أو صانع الجهاز) exact alarms لهذا التطبيق، فقد يجمّعها Doze بفارق دقائق. اطلب [requestExactAlarms] لإصلاحه. "unavailable" - Android 12+ وتعذّر الوصول إلى خدمة المنبّهات في النظام، فلا يمكن قراءة الدقة (وقد لا يُطلَق تذكير مجدول الآن أيضًا). على الصفحة التي يجب أن تكون في الوقت المحدد أن تفحص هذا بدل الافتراض. @return `"exact"` أو `"inexact"` أو `"unavailable"`.

مثال

يخبرك إن كانت التذكيرات ستُطلَق في الدقيقة بالضبط، أو قد تتأخر لأن المستخدم (أو صانع الهاتف) أوقف exact alarms لهذا التطبيق.

القيمة المُعادة: نص، بشكل متزامن: 'exact' أو 'inexact' (قد تجمّعه ميزة توفير البطارية، غالبًا بفارق دقائق) أو 'unavailable' (لا توجد خدمة منبّهات في الهاتف). المتطلبات: فعّل ⏰ Reminders في خطوة Access (الخطوة 4) عند الإنشاء.

function checkReminderTiming() {
  if (!(window.WebToApk && window.WebToApk.getAlarmPrecision)) return;
  const precision = WebToApk.getAlarmPrecision();
  const warn = document.getElementById('timing-warning');
  warn.hidden = precision === 'exact';
  if (precision === 'inexact') {
    warn.textContent = 'Reminders may be a few minutes late. Tap to allow exact alarms.';
    warn.onclick = function () { WebToApk.requestExactAlarms(); };
  }
}
checkReminderTiming();
// Re-check when the user returns from Settings:
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) checkReminderTiming();
});

افحص قبل ضبط منبّه استيقاظ، وأخبر المستخدم إن كان قد يتأخر:

function setWakeUp(atMs) {
  const b = window.WebToApk;
  if (!(b && b.getAlarmPrecision && b.scheduleNotificationEx)) return;
  const precision = b.getAlarmPrecision();
  if (precision === 'unavailable') { showMessage('Alarms cannot be set on this phone right now.'); return; }
  b.scheduleNotificationEx('wake', JSON.stringify({ at: atMs, title: 'Wake up', body: 'Good morning', openApp: true }));
  if (precision === 'inexact') {
    showMessage('Alarm set, but it may ring a few minutes late. Allow exact alarms for on-time alarms.');
  }
}

اعرض توقيت التذكير كسطر في شاشة الإعدادات:

function renderTimingRow() {
  if (!(window.WebToApk && window.WebToApk.getAlarmPrecision)) return;
  const labels = {
    exact: 'Reminders arrive on time',
    inexact: 'Reminders may be a few minutes late',
    unavailable: 'Reminders are not available on this phone'
  };
  document.getElementById('timing-row').textContent = labels[WebToApk.getAlarmPrecision()];
}
renderTimingRow();

ملاحظات: يجيب Android 11 والأقدم دائمًا 'exact'. ويظل تذكير 'inexact' يُطلَق، لكن ليس في الدقيقة بالضبط.

getAlarmSupport الجسر (bridge)#

window.WebToApk.getAlarmSupport(): String

هل يمكن جدولة `scheduleNotificationEx(id, {alarm: true, ...})` الآن: "ready" | "not_enabled" (Alarm clock متوقف في هذا الإنشاء) | "needs_exact_alarm" (استدعِ requestExactAlarms) | "needs_notification_permission".

مثال

يخبرك إن كان يمكن ضبط منبّه (scheduleNotificationEx(id, { alarm: true, ... })) على هذا الهاتف الآن، وإن لم يكن، ما الذي يجب أن يسمح به المستخدم أولًا.

القيمة المُعادة: نص، بشكل متزامن: 'ready'، أو 'not_enabled' (بُني التطبيق بدون 🔔 Alarm clock)، أو 'needs_exact_alarm' (Android 12+: exact alarms متوقفة لهذا التطبيق - استدعِ requestExactAlarms())، أو 'needs_notification_permission' (Android 13+: لا إذن إشعارات، فلن يوجد زر Dismiss - استدعِ requestNotificationPermission()). المتطلبات: فعّل 🔔 Alarm clock في خطوة Access (الخطوة 4) عند الإنشاء.

function prepareAlarms() {
  const b = window.WebToApk;
  if (!(b && b.getAlarmSupport)) { showMessage('Alarms work in the installed app only.'); return false; }
  switch (b.getAlarmSupport()) {
    case 'ready': return true;
    case 'needs_exact_alarm':
      showMessage('Allow "Alarms & reminders" for this app, then come back.');
      b.requestExactAlarms();
      return false;
    case 'needs_notification_permission':
      b.requestNotificationPermission();
      return false;
    default:
      showMessage('This app was built without alarm clocks.');
      return false;
  }
}

أعد الفحص عندما يعود المستخدم من الإعدادات:

document.addEventListener('visibilitychange', function () {
  if (document.hidden || !(window.WebToApk && window.WebToApk.getAlarmSupport)) return;
  document.getElementById('alarm-setup').hidden = WebToApk.getAlarmSupport() === 'ready';
});

أنشئ منبّهًا فقط عندما يكون الهاتف جاهزًا:

function setAlarmIn(hours) {
  const b = window.WebToApk;
  if (!(b && b.getAlarmSupport)) return false;
  if (b.getAlarmSupport() !== 'ready') { prepareAlarms(); return false; }   // asks for what is missing
  return b.scheduleNotificationEx('wake-up', JSON.stringify({
    at: Date.now() + hours * 3600 * 1000,
    alarm: true,
    title: 'Wake up',
    body: 'Good morning'
  }));
}

ملاحظات: يعيد scheduleNotificationEx مع alarm: true القيمة false كلما لم تكن هذه 'ready' - فلا يُخزَّن أبدًا منبّه لا يمكنه أن يرنّ.

getNotificationPermission الجسر (bridge)#

window.WebToApk.getNotificationPermission(): String

إذن الإشعارات الحالي، يُقرأ مباشرة من النظام. كان الـ polyfill يثبّت `Notification.permission = 'default'` عند كل تحميل للصفحة، فلم يكن النمط الشائع جدًا if (Notification.permission === 'granted') new Notification(...) يُطلَق أبدًا - حتى لمستخدم منح الإذن بالفعل، وحتى في Android 12 وما دونه حيث لا تحتاج الإشعارات إلى منحة وقت التشغيل أصلًا.

مثال

يقرأ، الآن، هل يمكن لهذا التطبيق نشر إشعارات.

القيمة المُعادة: نص، بشكل متزامن: 'granted'؛ 'denied' (رفض المستخدم نهائيًا، أو أوقف إشعارات هذا التطبيق من الإعدادات - فلن يظهر ما يُنشَر)؛ أو 'default' (لم يُحسم بعد، ولا يزال بإمكان التطبيق السؤال). المتطلبات: لا شيء لاستدعائه؛ أما النشر فيحتاج تفعيل Notifications في خطوة Access (الخطوة 4) عند الإنشاء.

واجهة الويب القياسية تقرأ القيمة الحية نفسها:

if ('Notification' in window && Notification.permission === 'granted') {
  document.getElementById('alerts-on').hidden = false;
}

الاستدعاء الخام، مثلًا بعد أن يعود المستخدم من الإعدادات:

function refreshAlertSetting() {
  if (!(window.WebToApk && window.WebToApk.getNotificationPermission)) return;
  const perm = WebToApk.getNotificationPermission();   // 'granted' | 'denied' | 'default'
  document.getElementById('enable-alerts').hidden = perm !== 'default';   // can still ask
  document.getElementById('open-settings-hint').hidden = perm !== 'denied';
}
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) refreshAlertSetting();
});
refreshAlertSetting();

قرّر قبل الجدولة هل تسأل أو تشرح أو تمضي:

async function ensureAlerts() {
  const b = window.WebToApk;
  if (!(b && b.getNotificationPermission)) return false;
  const perm = b.getNotificationPermission();
  if (perm === 'granted') return true;
  if (perm === 'denied') {
    showMessage('Notifications are off for this app. Turn them on in Settings.');
    return false;
  }
  // 'default': the app can still ask
  return (await Notification.requestPermission()) === 'granted';
}

ملاحظات: يعطي الإجابة نفسها كـ navigator.permissions.query({ name: 'notifications' }). لسؤال المستخدم، استخدم Notification.requestPermission().

getNotificationState الجسر (bridge)#

window.WebToApk.getNotificationState(): String

'granted' | 'prompt' (لا يزال Android يستطيع السؤال) | 'blocked' (رُفض نهائيًا أو أُوقف من الإعدادات - الإعدادات وحدها تصلحه) | 'unavailable' (Android 13+ وهذا الإنشاء لا يعلن إذن الإشعارات). انظر NotificationOptIn.

مثال

يقرأ، الآن، هل إشعارات هذا التطبيق تعمل - وإن لم تكن، هل لا يزال بإمكان التطبيق السؤال أم أن الإعدادات وحدها تعيدها.

القيمة المُعادة: نص، بشكل متزامن: 'granted' (تصل الإشعارات إلى الهاتف)؛ 'prompt' (لا يزال Android يستطيع عرض نافذة الإذن)؛ 'blocked' (رفض المستخدم نهائيًا، أو أوقف إشعارات هذا التطبيق من إعدادات النظام - الإعدادات وحدها تصلحه)؛ أو 'unavailable' (Android 13+ وبُني التطبيق بدون Notifications / Reminders / Push، فلا شيء يستطيع تشغيلها). المتطلبات: لا شيء لاستدعائه.

الاستدعاء الخام، لتقرر هل تعرض لافتة "Turn on notifications":

function refreshNotifBanner() {
  if (!(window.WebToApk && WebToApk.getNotificationState)) return;
  var state = WebToApk.getNotificationState();   // 'granted' | 'prompt' | 'blocked' | 'unavailable'
  var banner = document.getElementById('notif-banner');
  banner.hidden = !(state === 'prompt' || state === 'blocked');
  banner.querySelector('.hint').textContent = state === 'blocked'
    ? 'Notifications are off. Tap to open Settings and switch them on.'
    : 'Get order updates. Tap to turn on notifications.';
}
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) refreshNotifBanner();
});
refreshNotifBanner();

ملاحظات: يحتفظ getNotificationPermission() بكلماته الخاصة ('granted' / 'default' / 'denied') للصفحات الأقدم. لإعادة المستخدم، استدعِ AppMint.enableNotifications() (يختار لك النافذة أو الإعدادات).

getScheduledNotifications الجسر (bridge)#

window.WebToApk.getScheduledNotifications(): String

قراءة منحة POST_NOTIFICATIONS وطلبها، وإدارة الإشعارات التي جدولتها الصفحة بالفعل. النشر والجدولة موثّقان في طرقهما الخاصة.

موصوف عبر مجموعته، Notification permission, cancelling and listing، وليس بمفرده.

مثال

يسرد التذكيرات التي لا تزال مجدولة.

القيمة المُعادة: نص JSON، بشكل متزامن. حلّله. كل عنصر: { id, title, body, trigger, repeat, options? } - trigger هو وقت الإطلاق التالي بـ epoch ms، وrepeat هو none|minutely|hourly|daily|weekly، وoptions (لـ scheduleNotificationEx فقط) هي الخيارات كنص JSON. الحقول الحية تخبر كيف سيُسلَّم فعلًا، وتُقرأ مباشرة: precision ("exact"؛ "inexact" = "Alarms & reminders" متوقف للتطبيق، فيسلّمه Android ضمن نافذة 10 دقائق أثناء يقظة الهاتف، وقد يؤخره أكثر أثناء نومه؛ و"alarm_clock" لـ alarm: true)، وwindowMs + latestAt (النافذة التي أُعطيها Android: 0 / trigger عند exact، و600000 / trigger + 600000 عند inexact) وnotificationsEnabled (false = يُطلَق لكن Android لا يعرض شيئًا لأن إشعارات التطبيق متوقفة). '[]' عندما لا يوجد شيء. المتطلبات: لا شيء إضافي.

function renderReminders() {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return;
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }

  const ul = document.getElementById('reminders');
  ul.innerHTML = '';
  list.sort(function (a, b) { return a.trigger - b.trigger; }).forEach(function (r) {
    const li = document.createElement('li');
    li.textContent = r.title + ' — ' + new Date(r.trigger).toLocaleString() +
      (r.repeat !== 'none' ? ' (' + r.repeat + ')' : '');
    ul.appendChild(li);
  });
}

اعرض هل تذكير واحد يعمل، ومتى يُطلَق تاليًا:

function describeReminder(id) {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return 'Not available';
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  const r = list.find(function (x) { return x.id === id; });
  return r ? 'Next: ' + new Date(r.trigger).toLocaleString() : 'Off';
}
document.getElementById('water-status').textContent = describeReminder('water');

اقرأ الخيارات الإضافية للتذكيرات المنشأة بـ scheduleNotificationEx:

function listAlarmStyleReminders() {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return [];
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  return list.filter(function (r) {
    if (!r.options) return false;                  // made with scheduleNotification
    let o = {};
    try { o = JSON.parse(r.options); } catch (e) { return false; }
    return o.openApp === true;                     // puts the app on screen when it fires
  }).map(function (r) { return r.id; });
}

ملاحظات: التذكير لمرة واحدة يغادر القائمة عند إطلاقه. أما المتكرر فيبقى، وينتقل trigger إلى الوقت التالي. التذكير الذي فات وقته أثناء إيقاف الهاتف أو الإيقاف القسري للتطبيق يُعرض مرة واحدة، متأخرًا، عند التشغيل التالي للتطبيق أو الهاتف؛ وalarm: true المتأخر يُبلَّغ عنه كـ missed بدل أن يرنّ. التذكير اليومي أو الأسبوعي أو الشهري أو السنوي يحافظ على توقيته المحلي عندما يغيّر الهاتف المنطقة الزمنية.

حذّر عندما لن تظهر التذكيرات في وقتها:

function reminderHealth() {
  if (!(window.WebToApk && window.WebToApk.getScheduledNotifications)) return '';
  let list = [];
  try { list = JSON.parse(WebToApk.getScheduledNotifications() || '[]'); } catch (e) { list = []; }
  if (!list.length) return '';
  if (list.some(function (r) { return r.notificationsEnabled === false; })) return 'Notifications are off for this app: reminders will not appear.';
  if (list.some(function (r) { return r.precision === 'inexact'; })) return 'Reminders may arrive late. Turn on Alarms & reminders.';
  return '';
}

isAlarmRinging الجسر (bridge)#

window.WebToApk.isAlarmRinging(): Boolean

true أثناء رنين منبّه.

مثال

يخبرك إن كان أحد منبّهات التطبيق يرنّ الآن، ليتمكن للصفحة من عرض شاشة الإيقاف / الغفوة الخاصة بها.

القيمة المُعادة: true أثناء رنين منبّه، وإلا false (بشكل متزامن). المتطلبات: 🔔 Alarm clock في خطوة Access (الخطوة 4) عند الإنشاء.

function showRingScreenIfNeeded() {
  const b = window.WebToApk;
  const ringing = !!(b && b.isAlarmRinging && b.isAlarmRinging());
  document.getElementById('ring-screen').hidden = !ringing;
}
showRingScreenIfNeeded();
document.addEventListener('visibilitychange', showRingScreenIfNeeded);

أوقف المنبّه الراني من شاشتك بالاستدعاءات المقابلة.

document.getElementById('stop').addEventListener('click', function () {
  const b = window.WebToApk;
  if (b && b.isAlarmRinging && b.isAlarmRinging()) b.stopAlarm('');   // '' = whichever is ringing
});
document.getElementById('snooze').addEventListener('click', function () {
  const b = window.WebToApk;
  if (b && b.snoozeAlarm && !b.snoozeAlarm('', 10)) showMessage('Nothing is ringing');
});

تفاعل مع فتح التطبيق بواسطة منبّه يرنّ.

window.addEventListener('appmint:reminder', function (e) {
  if (e.detail.action === 'open' && e.detail.ringing) {
    document.getElementById('ring-screen').hidden = false;
  }
});

ملاحظات: عندما يفتح المنبّه الراني التطبيق، يصل appmint:reminder أيضًا مع { action: 'open', ringing: true }.

notify الجسر (bridge)#

window.WebToApk.notify(optionsJson: String): Boolean

إشعار غني. انظر NotificationBridge.show للقائمة الكاملة للخيارات: channel (urgent|default|quiet|ongoing)، image، largeIcon، bigText، ongoing، actions، progress، sound، silent، group، color. جلب الصور وفك ترميزها يتمان خارج الخيط الرئيسي، فلا يمكن لصورة بعيدة أن تحجب الصفحة.

مثال

ينشر إشعارًا فورًا، بأي مزيج من الخيارات: صورة، نص كبير، أزرار، شريط تقدم، ثابت (ongoing)، قناة، صوت، لون ووسم.

القيمة المُعادة: true عندما كانت الخيارات JSON صحيحًا وسُلّم الإشعار إلى النظام (يُبنى على خيط خلفي، فلا يحجب عنوان صورة سيئ الصفحة)؛ false لـ JSON غير صالح. المتطلبات: فعّل Notifications في خطوة Access (الخطوة 4) عند الإنشاء، وإذن المستخدم في Android 13+ (اطلبه بـ Notification.requestPermission()).

إشعار بسيط. tag هو معرّف: النشر مرة أخرى بالوسم نفسه يستبدل القديم.

async function notifyOrderReady() {
  if (!(window.WebToApk && window.WebToApk.notify)) return;
  if (window.Notification && Notification.permission !== 'granted') {
    const p = await Notification.requestPermission();
    if (p !== 'granted') { showMessage('Notifications are off for this app.'); return; }
  }
  WebToApk.notify(JSON.stringify({
    title: 'Order ready',
    body: 'Your order is ready to collect',
    channel: 'default',            // urgent | default | quiet | ongoing
    tag: 'order-42'
  }));
}

صورة وأيقونة كبيرة ونص طويل. يتغلب image على bigText عند إعطاء الاثنين.

WebToApk.notify(JSON.stringify({
  title: 'New photo',
  body: 'Tap to see it',
  image: 'https://example.com/photo.jpg',     // expanded big picture
  largeIcon: 'https://example.com/avatar.png', // round icon on the right
  color: '#00C6FF',
  tag: 'photo-7'
}));

WebToApk.notify(JSON.stringify({
  title: 'Weekly summary',
  body: '3 tasks done, 2 left',
  bigText: 'Done: pay rent, call bank, water plants. Left: tax form, dentist.',
  channel: 'quiet',                            // no sound, low priority
  group: 'summaries'
}));

أزرار (3 كحد أقصى). تعود النقرة كحدث appmint:notification-action.

WebToApk.notify(JSON.stringify({
  title: 'Ali sent you a message',
  body: 'Are we still on for 6pm?',
  channel: 'urgent',                           // heads-up banner
  tag: 'chat-ali',
  actions: [
    { id: 'open', label: 'Open chat' },
    { id: 'done', label: 'Mark read' }
  ]
}));

window.addEventListener('appmint:notification-action', function (e) {
  // e.detail = { actionId: 'done', tag: 'chat-ali' }
  if (e.detail.actionId === 'open') openChat(e.detail.tag);
  if (e.detail.actionId === 'done') markRead(e.detail.tag);
});

تقدم ثابت (ongoing). أعد النشر بالوسم نفسه لتحديثه. عند انتهاء العمل، انشر الحالة النهائية بـ ongoing: false ليتمكن المستخدم من إزاحته.

function showUpload(percent) {
  WebToApk.notify(JSON.stringify({
    title: 'Uploading', body: percent + '%',
    ongoing: true,                             // cannot be swiped away
    progress: { current: percent, max: 100 },  // or { indeterminate: true }
    silent: true,
    tag: 'upload'
  }));
}
showUpload(40);
// finished:
WebToApk.notify(JSON.stringify({ title: 'Upload complete', body: '3 photos saved', tag: 'upload' }));

خيارات أخرى: sound (ملف في حزمتك مثل '/audio/ding.mp3'، أو عنوان data:audio/...، أو 'silent')، smallIcon (اسم drawable)، when (epoch ms يُعرض كوقت)، openApp: true (تنبيه بملء الشاشة؛ انظر scheduleNotificationEx).

ملاحظات: الأزرار ليس لها مربع رد مضمّن، ولا تصل نقرة الزر إلى الصفحة إلا أثناء تشغيل التطبيق (ولا تفتح التطبيق)؛ ويستدعي الحدث أيضًا window.onAppMintNotificationAction(detail) إن عرّفته. يقبل image وlargeIcon عنوان https:// أو data:، أو صورة مضمّنة مع تطبيقك مسمّاة كما تسميها صفحتك ('img/logo.png'، أو '/img/logo.png'، أو عنوانها الكامل https://appassets.androidplatform.net/assets/…). الصورة السيئة تُتخطى ويظل الإشعار يظهر. يستخدم ongoing: true دائمًا قناة "Ongoing" الهادئة. ليس لـ notify() نظير عام "remove": فـ cancelNotification يلغي التذكيرات المجدولة فقط. الإشعار الذي تريد إزالته لاحقًا من الصفحة ينبغي نشره بـ new Notification(...) وإزالته بـ close()؛ وبطاقة تقدم ثابتة بـ Capacitor.AppwrightLiveProgress (end). يثبّت Android صوت القناة عند إنشائها، فقيمة sound جديدة تنشئ قناة جديدة.

openNotificationSettings الجسر (bridge)#

window.WebToApk.openNotificationSettings(): Boolean

يفتح إعدادات إشعارات هذا التطبيق؛ true عندما أمكن فتح الشاشة.

مثال

يفتح شاشة إعدادات إشعارات هذا التطبيق نفسه، حيث يمكن للمستخدم تشغيل الإشعارات (وكل قناة) أو إيقافها.

القيمة المُعادة: true عندما فُتحت الشاشة، بشكل متزامن؛ false عندما لا يمكن ذلك (Android 13+ والإنشاء بلا إشعارات). في Android 8+ هي صفحة إشعارات التطبيق؛ وفي الأقدم، شاشة App info. المتطلبات: لا شيء لتفعيله.

document.getElementById('notif-settings-link').onclick = function () {
  if (!(window.WebToApk && WebToApk.openNotificationSettings && WebToApk.openNotificationSettings())) {
    showMessage('Open Settings > Apps > this app > Notifications.');
  }
};

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

prepareNotificationSound الجسر (bridge)#

window.WebToApk.prepareNotificationSound(spec: String): Boolean

ينسخ ملف صوت مضمّنًا إلى مكانه مسبقًا ويخبر إن كان صالحًا كصوت إشعار. اختياري - فـ `notify()` و`scheduleNotificationEx()` يفعلان ذلك بنفسيهما - لكنه يتيح لشاشة إعدادات التحقق من صوت لحظة اختيار المستخدم له.

مثال

يتحقق من أن ملف صوت في تطبيقك يمكن استخدامه كصوت إشعار، وينسخه إلى مكانه الآن. اختياري: يفعل notify() وscheduleNotificationEx() ذلك بنفسيهما؛ وهذا يتيح لشاشة إعدادات فحص صوت لحظة اختيار المستخدم له.

القيمة المُعادة: true إن كان الصوت صالحًا؛ false إن كان الملف غير موجود أو ليس نوع صوت مدعومًا. المتطلبات: لا شيء.

function chooseAlarmSound(path) {             // e.g. '/audio/chime.mp3'
  if (!(window.WebToApk && window.WebToApk.prepareNotificationSound)) return;
  if (!WebToApk.prepareNotificationSound(path)) {
    showMessage('This sound file is missing from the app: ' + path);
    return;
  }
  localStorage.setItem('alarmSound', path);
  // Use it:
  WebToApk.notify(JSON.stringify({ title: 'Sound test', body: 'This is your alarm sound', sound: path, tag: 'sound-test' }));
}

املأ منتقي الأصوات بالأصوات الصالحة للاستخدام فعلًا فقط:

const SOUNDS = [
  { label: 'Chime', path: '/audio/chime.mp3' },
  { label: 'Bell',  path: '/audio/bell.ogg' },
  { label: 'Beep',  path: '/audio/beep.wav' }
];

function renderSoundPicker(select) {
  const b = window.WebToApk;
  if (!(b && b.prepareNotificationSound)) return;
  select.innerHTML = '';
  SOUNDS.forEach(function (s) {
    if (!b.prepareNotificationSound(s.path)) return;   // missing or unsupported file
    const opt = document.createElement('option');
    opt.value = s.path; opt.textContent = s.label;
    select.appendChild(opt);
  });
}

دع المستخدم يختار ملف الصوت الخاص به واستخدمه لتذكير يومي، ممرَّرًا كعنوان data::

document.getElementById('own-sound').addEventListener('change', function (e) {
  const file = e.target.files[0];
  const b = window.WebToApk;
  if (!file || !(b && b.prepareNotificationSound)) return;
  const reader = new FileReader();
  reader.onload = function () {
    const dataUrl = reader.result;                 // 'data:audio/mpeg;base64,...'
    if (!b.prepareNotificationSound(dataUrl)) {
      showMessage('That file cannot be used as a notification sound.');
      return;
    }
    b.scheduleNotificationEx('daily-walk', JSON.stringify({
      at: nextEvening(), repeat: 'daily',
      title: 'Walk time', body: 'Time for your evening walk', sound: dataUrl
    }));
  };
  reader.readAsDataURL(file);
});

القيم المقبولة: مسار في حزمة الويب لديك ('/audio/chime.mp3'، 'audio/chime.mp3') أو عنوان data:audio/...;base64,... أنشأته صفحتك. الأنواع: mp3 وwav وogg وoga وm4a وaac وflac وopus وmid وmidi.

ملاحظات: الاسم المجرد بدون مجلد أو امتداد (صوت res/raw) لا يُفحص هنا ويعيد false، رغم أن notify({sound: 'name'}) يستطيع استخدامه. يحتفظ Android بصوت واحد لكل قناة، فتغيير الصوت ينشئ قناة جديدة ويعيد ضبط إعدادات المستخدم لتلك القناة.

requestExactAlarms الجسر (bridge)#

window.WebToApk.requestExactAlarms(): Boolean

يفتح شاشة النظام التي تُمنح فيها exact alarms. Android 12+.

مثال

يفتح شاشة النظام في Android 12+ التي يسمح فيها المستخدم لهذا التطبيق باستخدام exact alarms، لتُطلَق التذكيرات في الدقيقة.

القيمة المُعادة: true عندما فُتحت شاشة الإعدادات (وفي Android 11 والأقدم، حيث لا حاجة لشيء)؛ false إن تعذّر فتح الشاشة. لا يخبرك باختيار المستخدم. المتطلبات: فعّل ⏰ Reminders في خطوة Access (الخطوة 4) عند الإنشاء.

document.getElementById('allow-exact').addEventListener('click', function () {
  if (!(window.WebToApk && window.WebToApk.requestExactAlarms)) return;
  if (WebToApk.getAlarmPrecision() === 'exact') return;   // already fine
  WebToApk.requestExactAlarms();
});

// The user comes back from Settings: read the real answer.
document.addEventListener('visibilitychange', function () {
  if (document.hidden || !window.WebToApk) return;
  const exact = WebToApk.getAlarmPrecision() === 'exact';
  document.getElementById('allow-exact').hidden = exact;
});

قبل حفظ تذكير حرج التوقيت، اعرض شاشة الإعدادات فقط عندما يكون التوقيت متوقفًا فعلًا:

function saveMedicineReminder(at) {
  const b = window.WebToApk;
  if (!(b && b.scheduleNotificationEx && b.getAlarmPrecision)) return;
  b.scheduleNotificationEx('medicine', JSON.stringify({
    at: at, repeat: 'daily', title: 'Medicine', body: 'Time for your tablet'
  }));
  const precision = b.getAlarmPrecision();   // 'exact' | 'inexact' | 'unavailable'
  if (precision === 'inexact') {
    showBanner('Reminders may arrive a few minutes late.', 'Fix timing', function () {
      b.requestExactAlarms();                // runs from the banner button tap
    });
  } else if (precision === 'unavailable') {
    showBanner('The phone\'s alarm service is not answering; this reminder may not fire.');
  }
}

عندما لا يمكن فتح شاشة الإعدادات (false)، أخبر المستخدم أين يجد المفتاح:

document.getElementById('fix-timing').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.requestExactAlarms)) return;
  const opened = b.requestExactAlarms();
  if (!opened) {
    showMessage('Open Settings > Apps > this app > "Alarms & reminders" and turn it on.');
  }
  // When it did open, read the answer on return with getAlarmPrecision().
});

ملاحظات: استدعه من نقرة زر واشرح السبب أولًا - فهو يغادر تطبيقك. التذكيرات المجدولة بالفعل تستمر في العمل في الحالتين؛ ويتغير توقيتها فقط.

requestNotificationPermission الجسر (bridge)#

window.WebToApk.requestNotificationPermission(): String

قراءة منحة POST_NOTIFICATIONS وطلبها، وإدارة الإشعارات التي جدولتها الصفحة بالفعل. النشر والجدولة موثّقان في طرقهما الخاصة.

موصوف عبر مجموعته، Notification permission, cancelling and listing، وليس بمفرده.

مثال

يعرض نافذة "Allow notifications?" في Android 13+. تستخدم الصفحات عادةً Notification.requestPermission()، الذي يستدعي هذا وينتظر إجابة المستخدم.

القيمة المُعادة: نص، بشكل متزامن: 'granted' إن كان مسموحًا بالفعل (أو في Android 12 والأقدم)، وإلا 'default' أثناء فتح النافذة. لا يستطيع إعادة إجابة المستخدم. المتطلبات: فعّل Notifications في خطوة Access (الخطوة 4) عند الإنشاء.

استخدم Promise API - فهو يُحَلّ بالإجابة الحقيقية ('granted' أو 'denied'):

document.getElementById('enable-alerts').addEventListener('click', async function () {
  if (!('Notification' in window)) return;
  const perm = await Notification.requestPermission();
  if (perm === 'granted') {
    showMessage('Alerts are on');
  } else {
    showMessage('Alerts are off. You can turn them on in Settings.');
  }
});

الاستدعاء الخام، فقط لفتح النافذة دون انتظار:

if (window.WebToApk && window.WebToApk.requestNotificationPermission) {
  const now = WebToApk.requestNotificationPermission();  // 'granted' or 'default'
  if (now === 'granted') enableAlertFeatures();
  // otherwise re-check later with WebToApk.getNotificationPermission()
  // ('granted' | 'denied' | 'default')
}

الشيفرة الأقدم التي تمرّر callback إلى Notification.requestPermission تحصل على الإجابة النهائية نفسها:

document.getElementById('turn-on-reminders').addEventListener('click', function () {
  if (!('Notification' in window)) return;
  if (Notification.permission !== 'default') { updateReminderUi(Notification.permission); return; }
  Notification.requestPermission(function (perm) {
    updateReminderUi(perm);   // 'granted' or 'denied', once the user answers
  });
});

ملاحظات: اطلب من نقرة زر مع سبب واضح. بعد أن يرفض المستخدم مرتين، يتوقف Android عن عرض النافذة وتكون الإجابة 'denied' فورًا؛ وعندها يجب على المستخدم السماح من إعدادات التطبيق.

requestOpenAppPermission الجسر (bridge)#

window.WebToApk.requestOpenAppPermission(): Boolean

يفتح شاشة النظام التي يُمنح فيها تنبيه ملء الشاشة. Android 14+.

مثال

يفتح شاشة النظام في Android 14+ التي يسمح فيها المستخدم بتنبيهات ملء الشاشة لهذا التطبيق، ليتمكن تذكير openApp: true من وضع التطبيق على الشاشة.

القيمة المُعادة: true عندما فُتحت شاشة الإعدادات (وفي ما دون Android 14، حيث لا حاجة لشيء)؛ false إن تعذّر فتحها. لا يخبرك باختيار المستخدم. المتطلبات: فعّل ⏰ Reminders في خطوة Access (الخطوة 4) عند الإنشاء.

document.getElementById('allow-open-app').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.requestOpenAppPermission)) return;
  if (b.canOpenAppAtTime() === 'granted') return;
  b.requestOpenAppPermission();
});

// Read the real answer when the user comes back:
document.addEventListener('visibilitychange', function () {
  if (document.hidden || !window.WebToApk) return;
  const ok = WebToApk.canOpenAppAtTime() === 'granted';
  document.getElementById('allow-open-app').hidden = ok;
});

تذكّر المنبّه الذي كان المستخدم يضبطه، وأرسله إلى الإعدادات، وأكمله عند عودته:

let pendingAlarm = null;

function setWakeAlarm(at) {
  const b = window.WebToApk;
  if (!(b && b.requestOpenAppPermission)) return;
  if (b.canOpenAppAtTime() === 'denied') {
    pendingAlarm = at;
    b.requestOpenAppPermission();
    return;
  }
  b.scheduleNotificationEx('wake', JSON.stringify({ at: at, title: 'Alarm', body: 'Wake up', openApp: true }));
}

document.addEventListener('visibilitychange', function () {
  if (document.hidden || pendingAlarm === null || !window.WebToApk) return;
  if (WebToApk.canOpenAppAtTime() === 'granted') setWakeAlarm(pendingAlarm);
  else showMessage('Without that setting the alarm cannot open the app by itself.');
  pendingAlarm = null;
});

عندما لا يمكن فتح شاشة الإعدادات (false)، قل أين يوجد المفتاح:

document.getElementById('allow-fullscreen').addEventListener('click', function () {
  const b = window.WebToApk;
  if (!(b && b.requestOpenAppPermission)) return;
  if (!b.requestOpenAppPermission()) {
    showMessage('Open Settings > Apps > this app > Notifications and allow full-screen notifications.');
  }
});

ملاحظات: استدعه من نقرة زر بعد شرح السبب - فهو يغادر تطبيقك.

scheduleNotification الجسر (bridge)#

window.WebToApk.scheduleNotification(id: String, title: String, body: String, triggerAtMillis: String, repeat: String): Boolean

قراءة منحة POST_NOTIFICATIONS وطلبها، وإدارة الإشعارات التي جدولتها الصفحة بالفعل. النشر والجدولة موثّقان في طرقهما الخاصة.

موصوف عبر مجموعته، Notification permission, cancelling and listing، وليس بمفرده.

مثال

يجدول تذكيرًا بسيطًا (عنوان ونص) لوقت مستقبلي. يُطلَق حتى والتطبيق مغلق ويُضبط من جديد بعد إعادة تشغيل الهاتف.

القيمة المُعادة: true عند الجدولة؛ false عندما يكون id فارغًا أو الوقت ليس رقمًا. المتطلبات: فعّل ⏰ Reminders في خطوة Access (الخطوة 4) عند الإنشاء (ويفعّل Notifications أيضًا)، وإذن إشعارات المستخدم في Android 13+.

function remindToDrinkWater() {
  if (!(window.WebToApk && window.WebToApk.scheduleNotification)) {
    showMessage('Reminders work in the installed app only.');
    return;
  }
  const ok = WebToApk.scheduleNotification(
    'water',                                  // your own id; same id replaces the old reminder
    'Drink water',                            // title
    'Time for a glass of water',              // body
    String(Date.now() + 60 * 60 * 1000),      // epoch milliseconds, AS A STRING
    'daily'                                   // none | minutely | hourly | daily | weekly
  );
  if (!ok) showMessage('Could not schedule the reminder');
}

اعرف متى فتح تذكير التطبيق (نقر عليه المستخدم):

window.addEventListener('appmint:reminder', function (e) {
  // e.detail = { id: 'water', overLockScreen: false }
  if (e.detail.id === 'water') openWaterLog();
});

تذكير لمرة واحدة في الساعة 8:00 من اليوم الذي تستحق فيه مهمة:

function remindTask(task) {
  if (!(window.WebToApk && window.WebToApk.scheduleNotification)) return false;
  const at = new Date(task.dueDate);
  at.setHours(8, 0, 0, 0);
  if (at.getTime() <= Date.now()) return false;     // that morning has passed
  return WebToApk.scheduleNotification(
    'task-' + task.id,
    'Due today',
    task.title,
    String(at.getTime()),                           // epoch ms as a string
    'none'
  );
}

ملاحظات: يستخدم التذكير أسلوب "urgent" (المنبثق) ووسمه هو المعرّف. التذكير المتكرر الذي فات أثناء إيقاف الهاتف يُطلَق في الخطوة التالية. لصورة أو أزرار أو صوتك الخاص أو فتح التطبيق فوق شاشة القفل استخدم scheduleNotificationEx. إن كانت exact alarms متوقفة للتطبيق فقد ينحرف الوقت بدقائق؛ افحص getAlarmPrecision().

scheduleNotificationEx الجسر (bridge)#

window.WebToApk.scheduleNotificationEx(id: String, optionsJson: String): Boolean

الصيغة الكاملة لـ [scheduleNotification]: كل ما يقبله `notify()`، يُطلَق في وقت مستقبلي والتطبيق مغلق. WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({ at: Date.now() + 3600000, // epoch millis (alias: triggerAt) repeat: 'daily', // none|minutely|hourly|daily|weekly|weekdays|monthly|yearly // days: [1, 3, 5], // with 'weekdays': ISO Mon=1..Sun=7 (default Mon-Fri) // alarm: true, // ring like an alarm clock (getAlarmSupport()) title: 'Time for your dose', body: 'Vitamin D - 1 tablet', sound: '/audio/chime.mp3', // a file in YOUR bundle; 'silent' for none openApp: true // open the app itself, over the lock screen })); يُنسخ الصوت المضمَّن إلى مكانه أثناء هذا الاستدعاء، فيُبلَّغ عن اسم ملف خاطئ الآن (false) بدل أن يتحول إلى الصوت الافتراضي بعد أيام.

مثال

يجدول تذكيرًا بكل الخيارات التي يملكها notify() (صورة، أزرار، صوت، قناة، فتح التطبيق)، لوقت مستقبلي. يُطلَق والتطبيق مغلق ويبقى بعد إعادة التشغيل.

القيمة المُعادة: true عند الجدولة. false عندما لا يُعطى وقت (at أو triggerAt أو triggerAtMillis أو inSeconds)، أو يكون المعرّف فارغًا، أو JSON سيئًا، أو يسمّي sound ملفًا غير موجود في تطبيقك. المتطلبات: فعّل ⏰ Reminders في خطوة Access (الخطوة 4) عند الإنشاء، وإذن إشعارات المستخدم في Android 13+.

تذكير يومي بصوتك الخاص يفتح التطبيق (فوق شاشة القفل):

function scheduleStandup(tomorrowAt9am) {
  const b = window.WebToApk;
  if (!(b && b.scheduleNotificationEx)) { showMessage('Reminders work in the installed app only.'); return; }

  // Both are user settings: check, and ask if needed.
  if (b.getAlarmPrecision() === 'inexact') b.requestExactAlarms();
  if (b.canOpenAppAtTime() === 'denied') b.requestOpenAppPermission();

  const ok = b.scheduleNotificationEx('standup', JSON.stringify({
    at: tomorrowAt9am,          // epoch ms (or triggerAt, or inSeconds: 3600)
    repeat: 'daily',            // none | minutely | hourly | daily | weekly | weekdays | monthly | yearly
    title: 'Stand-up',
    body: 'Daily sync starts now',
    sound: '/audio/alarm.mp3',  // a file in YOUR app; 'silent' for none
    openApp: true               // full-screen alert that puts the app on screen
  }));
  if (!ok) showMessage('Check the sound file path — nothing was scheduled.');
}

window.addEventListener('appmint:reminder', function (e) {
  // e.detail = { id: 'standup', overLockScreen: true, action: 'open', ringing: false }
  openMeetingScreen(e.detail.id);
});

أيام النادي فقط - الاثنين والأربعاء والجمعة في الساعة 18:00 (days بنظام ISO: الاثنين = 1 … الأحد = 7؛ وبدون days تعني 'weekdays' الاثنين-الجمعة):

WebToApk.scheduleNotificationEx('gym', JSON.stringify({
  at: nextAt(18, 0),          // epoch ms of the first time; moved to the next listed day if needed
  repeat: 'weekdays',
  days: [1, 3, 5],
  title: 'Gym time',
  body: 'Leg day'
}));

// Rent on the 31st: 'monthly' rings on the last day of shorter months (28/29/30) and on the 31st again when there is one.
WebToApk.scheduleNotificationEx('rent', JSON.stringify({ at: rentDueAt, repeat: 'monthly', title: 'Rent is due' }));
// A birthday: same date every year (29 February rings on the 28th in other years).
WebToApk.scheduleNotificationEx('bday-ana', JSON.stringify({ at: anaBirthdayAt9am, repeat: 'yearly', title: 'Ana\'s birthday' }));

منبّه حقيقي (alarm: true): يرنّ بمستوى صوت المنبّه في الهاتف - عبر الوضع الصامت، وعبر Do Not Disturb عندما يسمح المستخدم بالمنبّهات - ويستمر بالرنين حتى Dismiss / Snooze أو ringSeconds. يعرض أيقونة المنبّه في شريط الحالة ويظهر على الشاشة فوق شاشة القفل:

function setWakeAlarm(atMs) {
  const b = window.WebToApk;
  if (!(b && b.getAlarmSupport)) { showMessage('Alarms work in the installed app only.'); return; }
  const support = b.getAlarmSupport();   // 'ready' | 'not_enabled' | 'needs_exact_alarm' | 'needs_notification_permission'
  if (support === 'needs_exact_alarm') { b.requestExactAlarms(); return; }
  if (support !== 'ready') { showMessage('Alarms are not available: ' + support); return; }

  const ok = b.scheduleNotificationEx('wake', JSON.stringify({
    at: atMs,
    repeat: 'weekdays',          // daily/weekly/weekdays/monthly/yearly keep 07:00 across clock changes
    title: 'Wake up',
    body: 'Good morning',
    alarm: true,
    sound: '',                   // '' = the phone's alarm tone; or '/audio/rooster.mp3'; 'silent' = vibrate only
    vibrate: true,               // needs Vibration on in the build
    ringSeconds: 300,            // stop and count as missed after 5 minutes (default 600, max 3600)
    snoozeMinutes: 9,            // 0 hides the Snooze button (default 10)
    snoozeLabel: 'Snooze', dismissLabel: 'Stop'
  }));
  if (!ok) showMessage('The alarm was not set.');
}

window.addEventListener('appmint:reminder', function (e) {
  const d = e.detail;
  // Opened by the ringing alarm:  { id: 'wake', action: 'open', ringing: true, overLockScreen: true }
  // How a ring ended (also delivered later if the app was closed):
  //   { id: 'wake', action: 'dismiss' | 'snooze' | 'missed' | 'error', alarm: true,
  //     source: 'notification' | 'page' | 'timeout' | 'replaced' | 'system',
  //     snoozeMinutes?, snoozedUntil?, error?, soundError? }
  if (d.action === 'open' && d.ringing) showRingScreen(d.id);   // your Stop / Snooze buttons call stopAlarm / snoozeAlarm
  if (d.action === 'missed') logMissedAlarm(d.id);
});

تذكير بأزرار، بعد 30 دقيقة:

WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({
  inSeconds: 30 * 60,
  title: 'Time for your dose',
  body: 'Vitamin D — 1 tablet',
  actions: [{ id: 'taken', label: 'Taken' }, { id: 'snooze', label: 'Snooze' }]
}));

window.addEventListener('appmint:notification-action', function (e) {
  // e.detail = { actionId: 'taken', tag: 'dose-1' }
  if (e.detail.actionId === 'snooze') {
    WebToApk.scheduleNotificationEx(e.detail.tag, JSON.stringify({ inSeconds: 600, title: 'Time for your dose', body: 'Snoozed reminder' }));
  }
});

تذكير لمرة واحدة بصورة عند بدء تخفيض، على القناة العادية:

function remindSale(sale) {                        // sale.startsAt = epoch ms
  if (!(window.WebToApk && window.WebToApk.scheduleNotificationEx)) return false;
  return WebToApk.scheduleNotificationEx('sale-' + sale.id, JSON.stringify({
    at: sale.startsAt,
    title: 'The sale starts now',
    body: sale.name,
    image: 'https://example.com/banners/' + sale.id + '.jpg',   // expanded picture
    channel: 'default'                             // not the heads-up style reminders use by default
  }));
}

ملاحظات: المعرّف نفسه يستبدل تذكيرًا أقدم. الافتراضات: الوسم = المعرّف، القناة = urgent، وbigText = النص. نقرة الزر لا تصل الصفحة إلا أثناء تشغيل التطبيق. يُنسخ ملف الصوت عند الجدولة، فيفشل المسار الخاطئ الآن وليس بعد أيام؛ تعمل mp3 وwav وogg وm4a وaac وflac وopus. openApp هي الطريقة الوحيدة التي يظهر بها التطبيق على الشاشة من تلقاء نفسه في وقت محدد؛ وفي Android 14+ يجب أن يسمح المستخدم بذلك (canOpenAppAtTime). repeat: 'weekdays' مع days خارج 1-7 يعيد false. يحتاج alarm: true إلى 🔔 Alarm clock في خطوة Access (الخطوة 4) ويعيد false ما لم تكن getAlarmSupport() هي 'ready'؛ لا يرنّ إلا منبّه واحد في كل مرة (والجديد ينهي السابق كـ missed). إذا اختفى ملف صوت المنبّه نفسه عند رنينه (أزالته Remote Update)، يرنّ بنغمة المنبّه الافتراضية للهاتف ويحمل الحدث soundError: true.

showNotification الجسر (bridge)#

window.WebToApk.showNotification(title: String, body: String, icon: String, tag: String): Boolean

الصيغة الأصلية بأربع وسائط، محفوظة كما هي كمحوّل ليستمر عمل كل تطبيق موجود بالفعل وكل مقتطف في الأدلة. وهي الآن تمرّر إلى المُنشئ الغني - أي أن وسيط `icon`، الذي كان التنفيذ القديم يتجاهله بصمت، يفعل شيئًا أخيرًا.

مثال

استدعاء الإشعار الأصلي بأربع وسائط. يعرض إشعارًا بعنوان ونص وأيقونة كبيرة اختيارية ووسم. ينبغي للشيفرة الجديدة استخدام notify(optionsJson) أو new Notification(...) اللذين يفعلان أكثر.

القيمة المُعادة: true عندما سُلّم الإشعار إلى النظام. المتطلبات: فعّل Notifications في خطوة Access (الخطوة 4) عند الإنشاء، وإذن المستخدم في Android 13+.

واجهة الويب القياسية (تستدعي هذه الطريقة نيابةً عنك):

async function tellUser() {
  if (!('Notification' in window)) return;
  const p = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
  if (p === 'granted') new Notification('Saved', { body: 'Your note was saved', tag: 'note' });
}

الاستدعاء الخام. الوسائط: العنوان والنص والأيقونة والوسم. مرّر '' لعدم وجود أيقونة أو وسم.

if (window.WebToApk && window.WebToApk.showNotification) {
  WebToApk.showNotification(
    'Saved',                                  // title
    'Your note was saved',                    // body
    'https://example.com/avatar.png',         // large icon (https://, data:, or 'img/x.png' in your app), or ''
    'note'                                    // tag: same tag replaces the old notification
  );
}

حدّث إشعار حالة واحدًا في مكانه بإعادة استخدام وسمه:

function showSyncStatus(done, total) {
  const b = window.WebToApk;
  if (!(b && b.showNotification)) return;
  if (b.getNotificationPermission() !== 'granted') return;
  b.showNotification(
    'Syncing photos',
    done + ' of ' + total + ' uploaded',
    '',          // no large icon
    'sync'       // same tag every time: the text updates, no new notification
  );
}

ملاحظات: يمرّر إلى notify({title, body, largeIcon: icon, tag}) على القناة الافتراضية. بدون وسم يضيف كل استدعاء إشعارًا جديدًا. للصور أو الأزرار أو التقدم أو الإشعار الثابت استخدم notify.

snoozeAlarm الجسر (bridge)#

window.WebToApk.snoozeAlarm(id: String, minutes: Int): Boolean

يوقف المنبّه الراني [id] ("" = أيهما يرنّ) ويجعله يرنّ مجددًا بعد [minutes] (1-1440؛ 0 = snoozeMinutes الخاص بالمنبّه). false عندما لا يكون يرنّ.

مثال

يوقف منبّهًا يرنّ ويجعله يرنّ مرة أخرى بعد دقائق قليلة، مثل زر Snooze الخاص به. هذا الرنين وحده يتحرك: والمنبّه اليومي يظل يرنّ في وقته المعتاد غدًا.

القيمة المُعادة: true عندما كان المنبّه يرنّ وأُغفي؛ false عندما لم يكن يرنّ. id '' = أي منبّه يرنّ؛ minutes بين 1-1440، أو 0 لـ snoozeMinutes الخاص بالمنبّه (الافتراضي 10). المتطلبات: 🔔 Alarm clock في خطوة Access (الخطوة 4) عند الإنشاء.

document.getElementById('snooze').onclick = function () {
  if (!(window.WebToApk && window.WebToApk.snoozeAlarm)) return;
  if (WebToApk.snoozeAlarm('', 5)) showMessage('Ringing again in 5 minutes');
  hideRingScreen();
};

window.addEventListener('appmint:reminder', function (e) {
  // { id: 'wake', action: 'snooze', alarm: true, source: 'page', snoozeMinutes: 5, snoozedUntil: 1767340800000 }
  if (e.detail.action === 'snooze') updateNextAlarmLabel(e.detail.snoozedUntil);
});

جدول منبّهًا، ثم اربط زر إيقاف:

const b = window.WebToApk;
if (b && b.snoozeAlarm && b.getAlarmSupport() === 'ready') {
  b.scheduleNotificationEx('wake', JSON.stringify({
    at: Date.now() + 8 * 3600000, repeat: 'daily', alarm: true,
    title: 'Wake up', body: 'Good morning'
  }));
}
document.getElementById('dismiss').onclick = () => {
  if (WebToApk.isAlarmRinging()) WebToApk.stopAlarm('');
};

غفوة بطول المنبّه نفسه، والتعامل مع "nothing is ringing":

document.getElementById('snooze').onclick = () => {
  if (!(window.WebToApk && WebToApk.snoozeAlarm)) return;
  const ok = WebToApk.snoozeAlarm('', 0);   // 0 = the alarm's own snoozeMinutes (default 10)
  if (!ok) showMessage('No alarm is ringing right now.');
};

ملاحظات: يُدرِج getScheduledNotifications() رنين الغفوة كـ <id>#snooze؛ ويزيله cancelNotification(id) أيضًا.

stopAlarm الجسر (bridge)#

window.WebToApk.stopAlarm(id: String): Boolean

يوقف المنبّه الراني [id] ("" = أيهما يرنّ)، مثل زر Dismiss الخاص به. المنبّه المتكرر يبقى مجدولًا لوقته التالي. false عندما لا يكون يرنّ.

مثال

يوقف منبّهًا يرنّ، تمامًا مثل زر Dismiss الخاص به. المنبّه المتكرر يبقى مضبوطًا لوقته التالي؛ استخدم cancelNotification(id) لإزالته.

القيمة المُعادة: true عندما كان ذلك المنبّه يرنّ وأُوقف الآن؛ false عندما لم يكن يرنّ. مرّر '' لإيقاف أي منبّه يرنّ. المتطلبات: 🔔 Alarm clock في خطوة Access (الخطوة 4) عند الإنشاء.

window.addEventListener('appmint:reminder', function (e) {
  const d = e.detail;                 // { id, action: 'open', ringing: true, overLockScreen }
  if (d.action === 'open' && d.ringing) showRingScreen(d.id);
});

document.getElementById('stop').onclick = function () {
  if (window.WebToApk && window.WebToApk.stopAlarm) WebToApk.stopAlarm(currentAlarmId);
  hideRingScreen();
};

اعرض زر الإيقاف فقط حيث يوجد الـ bridge، واقرأ الإجابة:

const stopBtn = document.getElementById('stop');
if (window.WebToApk && typeof WebToApk.stopAlarm === 'function') {
  stopBtn.onclick = function () {
    const wasRinging = WebToApk.stopAlarm('');      // '' = whichever alarm is ringing
    if (!wasRinging) showMessage('Nothing was ringing.');
  };
} else {
  stopBtn.hidden = true;                             // plain browser: no alarm clock
}

اقرنه بـ isAlarmRinging() لتتمكن الصفحة من عرض شاشة الرنين لرنين بدأ أثناء غيابها:

document.addEventListener('visibilitychange', function () {
  if (document.hidden || !window.WebToApk) return;
  if (WebToApk.isAlarmRinging && WebToApk.isAlarmRinging()) showRingScreen();
  else hideRingScreen();
});

ملاحظات: فتتلقى الصفحة حينها أيضًا appmint:reminder مع { id, action: 'dismiss', alarm: true, source: 'page' }.

يتم إنشاؤه من بيئة تشغيل التطبيق وملفات الأمثلة الخاصة به عند كل بناء للمستندات. اقرأه بصيغة Markdown · جميع العائلات.

تمت المراجعة مقابل الجسر المُرفق بتاريخ 2026-10-22.