أنشئ APK مجانًا

واجهة JavaScript bridge API

تقارير الأعطال وسلامة الجهاز

الأخطاء التي تسجلها الصفحة، وآخر عطل أصلي، وما إذا كان الهاتف يبدو مُروَّتًا أو معدّلًا، مع رمز Play Integrity اختياري لخادمك.

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

AppMint.integrity.check() / AppMint.integrity.token(nonce, cloudProjectNumber?)

مثال

نسخ Promise من getDeviceIntegrity وrequestIntegrityToken، موجودة قبل أول سكربت في صفحتك عندما يفعّل الإنشاء فحوص سلامة الجهاز (خطوة التكامل، الخطوة 3).

يعيد: يحل check() بكائن الاستدلالات ({rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'})؛ ويحل token(nonce) بسلسلة رمز Play Integrity، أو يُرفض بـ Error رسالته اسم الخطأ (not_enabled، nonce_too_short، network_error، …) وcode رمز خطأ Play. الاستدلالات تلميحات؛ وحده فك الخادم للرمز هو الحكم.

async function guard() {
  if (!(window.AppMint && AppMint.integrity)) return;          // browser, or option off
  const local = await AppMint.integrity.check();
  if (local.rooted) console.warn('rooted signals:', local.signals);

  const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
  try {
    const token = await AppMint.integrity.token(nonce);          // project number from the build
    await fetch('/api/integrity/verify', { method: 'POST', body: token });
  } catch (e) {
    console.warn('Play Integrity unavailable:', e.message, e.code);
  }
}

نبّه عندما يكون التطبيق نسخة معاد تغليفها:

if (window.AppMint && AppMint.integrity) {
  const d = await AppMint.integrity.check();
  if (!d.installedFromPlay) console.log('installed by', d.installer || 'side-loaded');
  if (d.signatureSha256 && d.signatureSha256 !== MY_CERT_SHA256) reportTamper(d.signatureSha256);
}

عالج فشل الرمز بالاسم:

async function getToken(nonce) {
  if (!(window.AppMint && AppMint.integrity)) return null;
  try {
    return await AppMint.integrity.token(nonce);
  } catch (e) {
    if (e.message === 'not_enabled') console.warn('Device integrity checks are off in this build');
    else if (e.message === 'nonce_too_short') console.warn('the server nonce must be longer');
    else console.warn('Play Integrity error', e.message, e.code);
    return null;
  }
}

getDeviceIntegrity الجسر (bridge)#

window.WebToApk.getDeviceIntegrity(): String

استدلالات الروت / المحاكي / المُنقِّح المحلية (انظر DeviceIntegrity) بصيغة JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. النتيجة النظيفة تعني "لم تُوجد علامة"، لا إثباتًا - ولحكم يمكنك الوثوق به، أرسل رمز Play Integrity إلى خادمك (requestIntegrityToken). `{"error":"not_enabled"}` ما لم يفعّل الإنشاء سلامة الجهاز. يحجب لبضعة أجزاء من الثانية (يقرأ ملفات): استدعِه مرة واحدة.

مثال

استدلالات الروت والمحاكي والمُنقِّح المحلية كسلسلة JSON. النتيجة النظيفة تعني "لم تُوجد علامة"، لا إثباتًا: مستخدم مصمم مع وحدة إخفاء الروت يجتازها. استخدمه للتنبيه أو لإضافة فحص؛ ولحكم يمكنك الوثوق به، أرسل رمز Play Integrity إلى خادمك (requestIntegrityToken).

يعيد: '{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}' أو '{"error":"not_enabled"}'. تسمّي الإشارات ما وُجد: test-keys، su-binary:/system/xbin/su، root-path:/data/adb/magisk، root-app:com.topjohnwu.magisk، system-writable، ro.debuggable=1، ro.secure=0، selinux-permissive، bootloader-unlocked، emulator:*، debugger-attached. يتطلب: فعّل فحوص سلامة الجهاز في خطوة التكامل (الخطوة 3). يستغرق بضعة أجزاء من الثانية (يقرأ ملفات): استدعِه مرة واحدة، لا في حلقة.

function deviceCheck() {
  if (!(window.WebToApk && window.WebToApk.getDeviceIntegrity)) return null;
  const r = JSON.parse(WebToApk.getDeviceIntegrity());
  if (r.error) return null;                       // 'not_enabled'
  return r;
}

const d = deviceCheck();
if (d && d.rooted) {
  showNotice('This phone looks rooted (' + d.signals.join(', ') + '). Some features are turned off.');
}
if (d && !d.installedFromPlay) console.log('installed by', d.installer || 'unknown (side-loaded)');

تحقق من توقيعك - تُوقَّع النسخة المعاد تغليفها من تطبيقك بمفتاح آخر:

const MY_CERT = 'AB:CD:EF:…';   // Play Console → App integrity → App signing key SHA-256
const d = deviceCheck();
if (d && d.signatureSha256 && d.signatureSha256 !== MY_CERT) reportTamper();

اطلب من خادم حكمًا موثوقًا برمز Play Integrity (تصل الإجابة كحدث).

window.addEventListener('appmint:integrity', function (e) {
  if (e.detail.error) { showNotice('Check failed: ' + e.detail.error); return; }
  fetch('https://api.example.com/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token: e.detail.token })
  });
});
if (window.WebToApk && WebToApk.requestIntegrityToken) {
  const nonce = crypto.randomUUID().replace(/-/g, '') + '0000';   // your server issues and checks this
  WebToApk.requestIntegrityToken(nonce, '123456789012');          // your Google Cloud project number
}

getLastCrash الجسر (bridge)#

window.WebToApk.getLastCrash(): String

أحدث عطل سجّله هذا التثبيت (إغلاق التطبيق عند خطأ غير ملتقط، أو انهيار عارض الصفحة)، بصيغة JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`، أو "" عندما لا يوجد (أو تقارير الأعطال معطّلة). يبقى بعد الرفع، فيمكن للصفحة أن تقول "آسفون، أُغلق التطبيق في المرة الماضية".

مثال

أحدث عطل سجّله هذا التثبيت - إغلاق التطبيق عند خطأ غير ملتقط (kind: "native") أو انهيار عارض الصفحة (kind: "renderer") - كسلسلة JSON، أو '' عندما لا يوجد.

يعيد: '{"kind","message","stack","url","at","count","appVersion","user","fp"}' أو ''. وكذلك '' عندما لا يفعّل الإنشاء تقارير الأعطال. يتطلب: تقارير الأعطال مفعّلة في خطوة التكامل (الخطوة 3).

اعتذر مرة واحدة بعد عطل:

(function () {
  if (!(window.WebToApk && window.WebToApk.getLastCrash)) return;
  const raw = WebToApk.getLastCrash();
  if (!raw) return;
  const crash = JSON.parse(raw);
  if (localStorage.getItem('seenCrash') === String(crash.at)) return;   // shown already
  localStorage.setItem('seenCrash', String(crash.at));
  showBanner('Sorry, the app closed unexpectedly last time. We have been told about it.');
})();

ارفع العطل المخزّن إلى خادمك مرة واحدة، مع إصدار التطبيق الذي حدث عليه.

(function () {
  if (!(window.WebToApk && WebToApk.getLastCrash)) return;
  const raw = WebToApk.getLastCrash();
  if (!raw) return;                         // no crash, or Crash reports are off
  const c = JSON.parse(raw);
  if (localStorage.getItem('sentCrash') === String(c.at)) return;
  fetch('https://api.example.com/crashes', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ kind: c.kind, message: c.message, version: c.appVersion, count: c.count })
  }).then(function () { localStorage.setItem('sentCrash', String(c.at)); });
})();

فرّع حسب النوع لإعطاء رسالة مناسبة.

const raw = window.WebToApk && WebToApk.getLastCrash && WebToApk.getLastCrash();
if (raw) {
  const c = JSON.parse(raw);
  showBanner(c.kind === 'renderer'
    ? 'The page ran out of memory last time. Try closing other apps.'
    : 'The app closed unexpectedly last time.');
}

ملاحظات: يبقى بعد رفع التقرير، فقارن at بما خزّنته لعرض الرسالة مرة واحدة. أخطاء الصفحة واستدعاءات logError ليست أعطالًا ولا تظهر هنا أبدًا.

logError الجسر (bridge)#

window.WebToApk.logError(message: String, stack: String): Boolean

تقارير الأعطال: يسجّل خطأً التقطته صفحتك (فشل استدعاء دفع، خطأ تحليل) ليظهر في شاشة تقارير الأعطال في AppMint بجانب الأعطال، مجمّعًا ومعدودًا. [stack]: `err.stack` أو "". يعيد false عندما لا يفعّل الإنشاء تقارير الأعطال، أو عندما يكون كلاهما فارغًا، أو عندما سُجّل الخطأ نفسه مرات كثيرة في هذه الجلسة. لا تضع بيانات شخصية (بريد، هاتف، رموز) في الرسالة.

مثال

يسجّل خطأً التقطته صفحتك (فشل استدعاء دفع، خطأ تحليل JSON) ليظهر في شاشة تقارير الأعطال في AppMint (تحديث التطبيق ← تطبيقك ← تقارير الأعطال)، مجمّعًا مع الأعطال ومعدودًا. تُسجَّل الأخطاء غير الملتقطة ورفض الوعود غير المعالج تلقائيًا؛ وهذا للأخطاء التي عالجتها.

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

سجّل خطأً تمت معالجته:

function logError(err, context) {
  if (!(window.WebToApk && window.WebToApk.logError)) return false;   // browser, or an older build
  var msg = (context ? context + ': ' : '') + (err && err.message ? err.message : String(err));
  return WebToApk.logError(msg, (err && err.stack) || '');
}

async function checkout(cart) {
  try {
    await payments.charge(cart.total);
  } catch (err) {
    logError(err, 'checkout');          // shows up as "Logged by your page"
    showToast('Payment failed, please try again');
  }
}

أبلغ عن استدعاءات API الفاشلة دون سلسلة استعلام العنوان (قد تحمل رموزًا):

async function api(path) {
  const res = await fetch('/api' + path);
  if (!res.ok && window.WebToApk && WebToApk.logError) {
    WebToApk.logError('API ' + res.status + ' ' + path.split('?')[0], '');
  }
  return res;
}

سجّل خطأً ملتقطًا مع معرّف مستخدم، لتجد لاحقًا مشكلات مستخدم بعينه.

if (window.WebToApk && WebToApk.setCrashUserId) WebToApk.setCrashUserId('user_' + currentUser.id);

try {
  JSON.parse(savedState);
} catch (err) {
  if (window.WebToApk && WebToApk.logError) {
    const logged = WebToApk.logError('state parse: ' + err.message, err.stack || '');
    if (!logged) console.warn('not recorded (Crash reports off, or repeated too often)');
  }
}

ملاحظات: تُجمَّع التقارير ذات السبب نفسه حتى لو اختلفت الأرقام في الرسالة (item 42 وitem 7). يُخزَّن عنوان الصفحة بدون استعلامه وجزئه.

requestIntegrityToken الجسر (bridge)#

window.WebToApk.requestIntegrityToken(nonce: String, cloudProjectNumber: String): String

يطلب من Google Play رمز Play Integrity (طلب كلاسيكي). [nonce]: Base64 آمن للعناوين، من 16 إلى 500 حرف، ينشئه خادمك لهذا الطلب وحده. [cloudProjectNumber]: رقم مشروع Google Cloud الخاص بك، أو "" لاستخدام الرقم المضبوط عند الإنشاء (التطبيق المثبّت من Play يمكنه ترك كليهما فارغًا). يعيد معرّف طلب فورًا ("" إذا لم يفعّل الإنشاء فحوص سلامة الجهاز)؛ وتصل الإجابة كـ `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` أو `{requestId, error, code}`. أرسل الرمز إلى خادمك، الذي يفك ترميزه مع Google (playintegrity.googleapis.com … :decodeIntegrityToken) ويتحقق من nonce.

مثال

يطلب من Google Play رمز Play Integrity (طلب كلاسيكي). الرمز معتم على الهاتف: يرسله خادمك إلى Google، ويقرأ الحكم (تطبيق أصلي؟ جهاز أصلي؟ مرخّص؟) ويتحقق من أن nonce هو الذي أصدره. هذا التحقق من الخادم هو ما يجعل الإجابة موثوقة.

يعيد: معرّف طلب فورًا ('' إذا لم يفعّل الإنشاء سلامة الجهاز). تصل الإجابة كحدث appmint:integrity (وكذلك window.onAppMintIntegrity) مع {requestId, token} أو {requestId, error, code} - أخطاء مثل nonce_too_short، nonce_is_not_base64، network_error، play_store_not_found، cloud_project_number_is_invalid، too_many_requests. يتطلب: فحوص سلامة الجهاز مفعّلة في خطوة التكامل (الخطوة 3)، وتفعيل واجهة Play Integrity API للتطبيق (Play Console ← سلامة التطبيق)، و- لتطبيق غير مثبّت من Google Play - رقم مشروع Google Cloud الخاص بك، في المعالج أو كوسيط ثانٍ (يستخدم '' رقم الإنشاء).

async function integrityToken() {
  // 1. Your server makes a one-time nonce: URL-safe Base64, 16-500 characters.
  const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
  return new Promise((resolve, reject) => {
    const id = WebToApk.requestIntegrityToken(nonce, '');
    if (!id) return reject(new Error('not_enabled'));
    window.addEventListener('appmint:integrity', function on(e) {
      if (e.detail.requestId !== id) return;
      window.removeEventListener('appmint:integrity', on);
      e.detail.token ? resolve(e.detail.token) : reject(new Error(e.detail.error));
    });
  });
}

// 2. Your server decodes it with Google and decides.
const token = await integrityToken();
const verdict = await fetch('/api/integrity/verify', { method: 'POST', body: token }).then(r => r.json());

خطوة الخادم - استدعِ POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken مع {"integrity_token": token} باستخدام حساب خدمة من مشروع Cloud المرتبط، ثم تحقق:

const p = decoded.tokenPayloadExternal;
const ok = p.requestDetails.nonce === issuedNonce &&
  p.appIntegrity.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
  p.deviceIntegrity.deviceRecognitionVerdict.includes('MEETS_DEVICE_INTEGRITY');

صيغة رد النداء، للشيفرة التي لا تستطيع الاستماع للأحداث:

window.onAppMintIntegrity = function (d) {
  if (d.error) console.warn('integrity failed', d.error, d.code);
  else sendToServer(d.token);
};
WebToApk.requestIntegrityToken(nonceFromServer, '123456789012');

setCrashUserId الجسر (bridge)#

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

تقارير الأعطال: معرّفك للمستخدم المسجّل (حروف وأرقام و _ @ . : + -، حتى 128)، يُرفق بالتقارير من الآن فصاعدًا لتعدّ المستخدمين المتأثرين وتجد أعطال مستخدم بعينه. "" يمسحه. ليس أبدًا عنوان بريد أو رقم هاتف. يعيد false لمعرّف بأحرف أخرى، أو عندما تكون تقارير الأعطال معطّلة.

مثال

يربط معرّفك للمستخدم المسجّل بتقارير الأعطال من الآن فصاعدًا، لتتمكن شاشة تقارير الأعطال من عدّ المستخدمين المتأثرين بمشكلة. استدعِه بعد تسجيل الدخول؛ واستدعِه بـ '' عند تسجيل الخروج.

يعيد: true عند الحفظ؛ false لمعرّف يحوي أحرفًا غير الحروف والأرقام و_ @ . : + - (أو أطول من 128)، أو عندما لا يفعّل الإنشاء تقارير الأعطال. يتطلب: تقارير الأعطال مفعّلة في خطوة التكامل (الخطوة 3). استخدم معرّف قاعدة بياناتك، لا عنوان بريد إلكتروني أو رقم هاتف.

function crashUser(id) {
  if (!(window.WebToApk && window.WebToApk.setCrashUserId)) return false;
  return WebToApk.setCrashUserId(id);
}

async function signIn(email, password) {
  const user = await myBackend.signIn(email, password);
  crashUser(user.id);            // e.g. 'u_42'
}

function signOut() {
  crashUser('');                 // later reports carry no user
}

المعرّف المرفوض، أو إنشاء بلا تقارير أعطال، يعيد false:

function setCrashUser(id) {
  if (!(window.WebToApk && WebToApk.setCrashUserId)) return;
  if (!WebToApk.setCrashUserId(id)) {
    console.warn('Crash user id not saved: bad characters, over 128 long, or Crash reports are off');
  }
}
setCrashUser('u_42');       // your database id, never an e-mail address

أخبر المستخدم بآخر عطل بعد إعادة التشغيل:

if (window.WebToApk && WebToApk.getLastCrash) {
  const raw = WebToApk.getLastCrash();          // '' when there has been none
  if (raw) {
    const c = JSON.parse(raw);
    showMessage('Sorry, the app closed last time (' + c.kind + ').');
  }
}

ملاحظات: يبقى المعرّف على الهاتف حتى تغيّره ولا يُرسل إلا داخل تقارير الأعطال.

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

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