> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# النشر في App Store و Google Play: التوقيع Signing والمراجعة
- URL: https://arabroot.io/articles/نشر-تطبيقات-الجوال-في-app-store-و-google-play/
- Published: 2026-10-30T19:10:33.000Z
- Updated: 2026-10-30T22:39:03.000Z
- Description: نوقع إصداراً حقيقياً لتطبيق Android ونرى ماذا يرفض الجهاز ولماذا، ثم نمر على ما تطلبه Google وApple في 2026 وأسباب الرفض الشائعة وكيف تتجنبها قبل أول إرسال.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, تطوير الواجهات والجوال Frontend & Mobile

لنفرض أن لديك فريقاً صغيراً من ثلاثة مطورين أنهى تطبيق طلبات لمتجر إلكتروني ناشئ، والتطبيق يعمل على جوالات الفريق منذ أسابيع، والعميل حدد موعد الإطلاق يوم الأحد القادم. وفي يوم الخميس يكتشف أحدهم أن كل نسخ الاختبار كانت موقعة بمفتاح التصحيح Debug Key الموجود على جهازه، وأن أحداً لم ينشئ مفتاح الإصدار بعد، وأن حساب Google Play أنشئ قبل أسبوع فقط، وأن Apple طلبت في رسالة الرفض الأولى حساباً تجريبياً لتسجيل الدخول لم يرسله أحد. وهذه ليست قصة نادرة، فالفرق تقضي شهوراً في بناء التطبيق ثم تكتشف أن الخطوة الأخيرة، أي إيصاله إلى المتجرين، لها قواعدها الخاصة ومواعيدها التي لا تتحرك من أجل موعد إطلاقك.

والمشكلة أن هذه الخطوة تجمع ثلاثة أشياء مختلفة في وقت واحد: التوقيع الرقمي Code Signing الذي يربط كل تحديث قادم بمفتاح واحد لا يمكن تغييره بسهولة، والتغليف Packaging الذي يحدد ما الذي يصل إلى جوال المستخدم وكم حجمه، والمراجعة Review التي يقرأ فيها إنسان أو نظام آلي تطبيقك مقابل قائمة طويلة من الإرشادات. وأي خطأ في الأول يبقى معك طوال عمر التطبيق، أما الخطآن الآخران فيكلفانك أياماً من التأخير في كل مرة.

وفي هذا المقال سوف نأخذ تطبيق الطلبات نفسه الذي بنيناه في مقالات هذه المرحلة، ونجهز له إصداراً حقيقياً لنظام Android على جهازنا: نولد مفتاح الرفع، ونرى ماذا يفعل R8 بالحجم، ونوقع ونتحقق، ثم نخطئ عمداً لنرى رسائل الرفض الحقيقية من الجهاز، ونجرب تدوير المفتاح، ونبني حزمة App Bundle ونرى ما الذي يصل إلى جوال واحد منها. ولاحظ أننا لم نرسل التطبيق إلى Play Console ولا إلى App Store Connect، فجزء المتاجر في المقال مبني على صفحاتها الرسمية مع تاريخ كل اقتباس، وجزء التوقيع والتغليف مبني على تجربة نفذناها فعلاً.

سوف نناقش في هذا المقال ما يلي:

- لماذا يوقع كل تطبيق جوال، وكيف تطورت مخططات التوقيع في Android من v1 إلى v3.1.
- الفرق بين مفتاح الرفع Upload Key ومفتاح توقيع التطبيق App Signing Key، وماذا يحدث إذا ضاع أحدهما.
- سلسلة التوقيع عند Apple: الشهادة والمعرف والملف التعريفي Provisioning Profile.
- تجربة كاملة: R8 والتوقيع والتحقق والعبث بالملف والتحديث بمفتاح مختلف وتدوير المفتاح وحزمة App Bundle ومحاذاة 16 KB.
- ما تطلبه Google Play في 2026: مستوى API المستهدف والاختبار المغلق وأمان البيانات والتحقق من هوية المطور.
- كيف تقرأ إرشادات مراجعة Apple، وما الذي يقوله تقرير الشفافية عن أسباب الرفض.
- جدول بأسباب الرفض الشائعة وكيف تتجنبها، وقائمة تحقق قبل الإرسال.

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

![مساران متوازيان: في Android بناء الحزمة ثم توقيعها بمفتاح الرفع ثم Play Console الذي يراجع ويعيد التوقيع ثم ملفات مقسمة على الجوال، وفي iOS أرشفة في Xcode ثم شهادة وملف تعريفي ثم App Store Connect وTestFlight والمراجعة ثم App Store](https://arabroot.io/content/images/2026/10/mobile-store-publishing-01-release-path.webp)

مسار الإصدار في Android وiOS: التوقيع قبل الرفع، والمراجعة بعده

## لماذا يوقع كل تطبيق؟ التوقيع الرقمي Code Signing

لنبدأ من السؤال الذي يطرحه كل من يرى خطوة التوقيع لأول مرة: لماذا يحتاج نظام التشغيل أن يعرف من كتب التطبيق؟ والإجابة على ذلك أن الجوال لا يهمه اسمك، وإنما يهمه أمر واحد هو أن التحديث القادم جاء من نفس الجهة التي جاء منها الإصدار المثبت. فإذا ثبت المستخدم تطبيق الطلبات وأعطاه صلاحية الإشعارات والموقع، وحفظ فيه التطبيق توكن تسجيل الدخول في ملفاته الخاصة، ثم وصل ملف APK آخر يحمل نفس اسم الحزمة Package Name ويريد أن يحل محله، فالنظام يجب أن يقرر: هل هذا تحديث حقيقي من المطور نفسه، أم تطبيق آخر يريد أن يرث بيانات الأول وصلاحياته؟ والطريقة الوحيدة التي يعرف بها ذلك هي التوقيع: كل إصدار يحمل توقيعاً بمفتاح خاص Private Key، والنظام يقارن شهادة التحديث بشهادة النسخة المثبتة، فإذا اختلفتا يرفض التحديث.

وهذا يعني أن التوقيع في Android لا يحتاج جهة إصدار شهادات Certificate Authority كما في مواقع الويب، فالشهادة غالباً موقعة ذاتياً Self-signed، وقيمتها في ثباتها لا في من أصدرها. ولذلك تقول صفحة [توقيع التطبيق](https://developer.android.com/studio/publish/app-signing?ref=arabroot.io) في توثيق Android إن مفتاح التوقيع لا يتغير طوال عمر التطبيق ضمن نموذج التحديث الآمن في النظام. أما في iOS فالأمر مختلف، فالشهادة تصدرها Apple نفسها لحسابك، ولا يقبل الجهاز أي تطبيق غير موقع بسلسلة تنتهي عند Apple، وسوف نرى ذلك بعد قليل.

### مخططات التوقيع Signature Schemes من v1 إلى v3.1

في بدايات Android كان التوقيع هو توقيع ملفات JAR المعروف من Java، ويسمى اليوم المخطط الأول v1: يحسب الهاش لكل ملف داخل الأرشيف ويضعه في ملفات داخل المجلد `META-INF` ثم يوقعها. وهذه الطريقة لها مشكلتان ذكرهما توثيق [توقيع APK](https://source.android.com/docs/security/features/apksigning?ref=arabroot.io) في مشروع Android: لا تحمي أجزاء من ملف ZIP مثل بياناته الوصفية، والجهاز يجب أن يفك ضغط كل الملفات ليتحقق منها، وهذا بطيء ويفتح مساحة للهجوم على المحلل نفسه. لذلك جاء المخطط الثاني [APK Signature Scheme v2](https://source.android.com/docs/security/features/apksigning/v2?ref=arabroot.io) في Android 7.0، وهو يوقع الملف كاملاً كسلسلة بايتات ويضع التوقيع في كتلة خاصة اسمها APK Signing Block بين محتوى الملفات والفهرس المركزي Central Directory، فأي تغيير في أي بايت يفسد التوقيع، والتحقق لا يحتاج فك الضغط.

ثم جاء [المخطط الثالث v3](https://source.android.com/docs/security/features/apksigning/v3?ref=arabroot.io) في Android 9، وهو نفس v2 مع معلومات إضافية أهمها إثبات التدوير proof-of-rotation، أي إمكانية أن ينتقل التطبيق من مفتاح قديم إلى مفتاح جديد بشرط أن يوقع القديم على الجديد. ثم أضاف Android 13 [المخطط v3.1](https://source.android.com/docs/security/features/apksigning/v3-1?ref=arabroot.io) الذي يسمح بأن يستخدم الجهاز المفتاح الجديد بدءاً من إصدار معين من النظام، وأن تبقى الأجهزة الأقدم على المفتاح القديم، وسوف نرى هذا بأعيننا في التجربة. وهناك أيضاً المخطط v4 الذي يضع التوقيع في ملف منفصل لخدمة التثبيت التدريجي Incremental Install، ولا يعنيك غالباً في النشر.

![ملف APK مقسم إلى محتوى الملفات ثم كتلة التوقيع ثم الفهرس المركزي ثم نهاية الملف، وتحتها مقارنة بين ملف يتحقق بعد التوقيع وملف أضيف إليه ملف واحد ففشل التحقق](https://arabroot.io/content/images/2026/10/mobile-store-publishing-03-signing-block.webp)

التوقيع v2 وما بعده يحمي كل بايت في الملف، وإضافة ملف واحد بعد التوقيع تفسده

## مفتاح الرفع Upload Key ومفتاح توقيع التطبيق App Signing Key

إذا كان المفتاح لا يتغير طوال عمر التطبيق، فهناك سؤال مخيف: ماذا يحدث إذا ضاع؟ قبل سنوات كان الجواب بسيطاً وقاسياً: لا يمكنك تحديث التطبيق أبداً، وعليك أن تنشر تطبيقاً جديداً باسم حزمة جديد وتطلب من المستخدمين الانتقال إليه، وتخسر التقييمات وعدد التنزيلات. ولذلك أضافت Google خدمة Play App Signing، والفكرة أن هناك مفتاحين: مفتاح توقيع التطبيق App Signing Key الذي يوقع به الملف الذي يصل إلى الجوال، وتحتفظ به Google في بنيتها، ومفتاح الرفع Upload Key الذي تحتفظ به أنت وتوقع به الحزمة قبل رفعها إلى Play Console فقط، لتثبت أنها جاءت منك.

وتشرح صفحة [توقيع التطبيق](https://developer.android.com/studio/publish/app-signing?ref=arabroot.io) الفائدة مباشرة: باستخدام مفتاح رفع منفصل يمكنك طلب إعادة تعيين مفتاح الرفع إذا ضاع أو انكشف، أما التطبيقات التي لم تشترك في Play App Signing فإذا فقدت مفتاح التوقيع تفقد القدرة على تحديث التطبيق. والخدمة اختيارية فقط للتطبيقات التي أنشئت قبل أغسطس 2021، لأن Google تطلب منذ ذلك الوقت أن تنشر التطبيقات الجديدة بصيغة [Android App Bundle](https://developer.android.com/guide/app-bundle?ref=arabroot.io)، والحزمة لا يمكن أن تصل إلى الجوال إلا بعد أن تقطعها Google إلى ملفات APK وتوقعها، وبالتالي يجب أن يكون مفتاح التوقيع عندها. وطلب إعادة تعيين مفتاح الرفع يتم من Play Console كما في [صفحة المساعدة الخاصة به](https://support.google.com/googleplay/android-developer/answer/9842756?ref=arabroot.io).

![الفريق يوقع الحزمة بمفتاح الرفع ويرسلها إلى Play App Signing الذي يحتفظ بمفتاح توقيع التطبيق ويوقع به ملفات APK التي تصل إلى جهاز المستخدم، والجهاز يقارن توقيع التحديث بتوقيع النسخة المثبتة](https://arabroot.io/content/images/2026/10/mobile-store-publishing-02-two-keys.webp)

مفتاح الرفع يثبت أن الحزمة منك، ومفتاح التطبيق هو ما يراه الجوال

وقد يتساءل البعض: هل يعني هذا أن Google تستطيع توقيع أي شيء باسم تطبيقي؟ نعم، وهذا هو الثمن، فأنت تثق بأن المتجر الذي يوزع تطبيقك لن يوزع غيره باسمك، وهي ثقة تعطيها أصلاً عندما تنشر فيه. وإذا كنت تريد نشر نفس التطبيق في متاجر أخرى بنفس التوقيع، فالتوثيق ينبه إلى أن ترفع مفتاحك أنت عند إعداد Play App Signing بدلاً من أن تترك Google تولد مفتاحاً جديداً، لأن المفتاح الذي تولده Google لا يمكنك إخراجه لتوقع به في مكان آخر.

### ماذا يحدث إذا ضاع المفتاح Key Loss؟

لنضع الحالات في جدول واحد، لأن الفرق يكتشفونها غالباً في أسوأ يوم ممكن:

| ما الذي ضاع                     | مع Play App Signing                                               | دون Play App Signing                             | في iOS                                                    |
| ------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
| مفتاح الرفع Upload Key          | تطلب إعادة تعيينه من Play Console، وتتوقف عن الرفع حتى يقبل الطلب | غير موجود، المفتاح الوحيد هو مفتاح التطبيق       | لا يوجد مقابل مباشر                                       |
| مفتاح توقيع التطبيق             | عند Google، لا تفقده                                              | لا يمكن تحديث التطبيق أبداً، تنشر تطبيقاً جديداً | تصدر شهادة جديدة من حسابك، فالتوقيع مرتبط بالحساب لا بملف |
| كلمة مرور ملف المفاتيح Keystore | نفس حالة ضياع مفتاح الرفع                                         | نفس حالة ضياع المفتاح                            | تعيد إصدار الشهادة                                        |

ولاحظ أن الفرق في iOS جوهري، فالجهاز لا يقارن مفتاحك بمفتاح سابق، وإنما يتحقق أن التطبيق موقع بسلسلة تنتهي عند Apple وأن معرف الفريق Team ID ومعرف الحزمة Bundle ID كما هما، ولذلك يمكنك إلغاء شهادة وإصدار أخرى دون أن يتأثر المستخدمون. وفي مقال [أسئلة مقابلة مطور الجوال](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A6%D9%84%D8%A9-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%85%D8%B7%D9%88%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AE%D8%A8%D9%8A%D8%B1/#mo-signing) سؤال كامل عن هذه الحالة، فراجعه إذا كنت تستعد لمقابلة.

## التوقيع عند Apple: الشهادات Certificates والملفات التعريفية Provisioning Profiles

في Apple لا يكفي مفتاح واحد، وإنما سلسلة من أربع حلقات يجب أن تتطابق كلها. الحلقة الأولى شهادة التوزيع Apple Distribution التي يصدرها حساب المطور، ومفتاحها الخاص يبقى في Keychain على جهاز Mac الذي أنشأ الطلب أو في نظام البناء، كما تشرح صفحة [نظرة عامة على الشهادات](https://developer.apple.com/help/account/certificates/certificates-overview/?ref=arabroot.io). والحلقة الثانية معرف التطبيق App ID، وهو معرف الفريق مع معرف الحزمة، ومعه القدرات Capabilities التي يستخدمها التطبيق مثل الإشعارات Push Notifications والنطاقات المرتبطة Associated Domains. والحلقة الثالثة الملف التعريفي Provisioning Profile، وهو ملف يربط الشهادة بالمعرف وبالقدرات، ونوعه للنشر في المتجر هو [App Store Provisioning Profile](https://developer.apple.com/help/account/provisioning-profiles/create-an-app-store-provisioning-profile/?ref=arabroot.io). والحلقة الرابعة الأرشيف الموقع الذي يخرج من Xcode ويرفع إلى App Store Connect.

![أربع حلقات من اليمين إلى اليسار: شهادة التوزيع ومفتاحها في Keychain، ثم معرف التطبيق مع القدرات، ثم الملف التعريفي الذي يربطهما، ثم الأرشيف الموقع الذي يرفع إلى App Store Connect](https://arabroot.io/content/images/2026/10/mobile-store-publishing-06-apple-chain.webp)

سلسلة التوقيع في iOS: كل حلقة يجب أن تطابق التي قبلها

والسبب في هذا التعقيد أن Apple تتحكم في كل ما يعمل على الجهاز، فالملف التعريفي هو الذي يقول للجهاز إن هذا التطبيق مسموح له باستخدام الإشعارات أو الروابط العامة، وإذا أضفت قدرة جديدة في الكود ولم تحدث الملف التعريفي فسوف يفشل الرفع أو يعمل التطبيق دون تلك القدرة. ولهذا السبب تحديداً ينصح الكثيرون بترك Xcode يدير التوقيع تلقائياً Automatic Signing في المشاريع الصغيرة، ولكن عليك أن تفهم الحلقات لأن البناء في خادم التكامل المستمر CI لا يملك Keychain جهازك، وهناك ستحتاج أن تنقل الشهادة والملف بنفسك. ولاحظ أن حساب [Apple Developer Program](https://developer.apple.com/programs/?ref=arabroot.io) باشتراك سنوي قيمته 99 دولاراً، بينما رسوم [التسجيل في Google Play](https://support.google.com/googleplay/android-developer/answer/6112435?ref=arabroot.io) 25 دولاراً تدفع مرة واحدة، وكلاهما يحتاج تحقق هوية قد يأخذ أياماً، فابدأ به قبل موعد الإطلاق بوقت كاف.

## تجربة عملية Lab: تجهيز إصدار Release لتطبيق Android وتوقيعه

الآن سوف نطبق كل ما سبق على تطبيق الطلبات المكتوب بلغة Kotlin وواجهة Jetpack Compose، وهو التطبيق نفسه الذي قارناه بنسختي Flutter وReact Native في مقال اختيار التقنية من هذه المرحلة. وكل الأدوات من حزمة Android SDK الرسمية: `keytool` و`jarsigner` من JDK المرفق مع Android Studio، و`apksigner` و`zipalign` و`aapt2` من Build-Tools 37.0.0، والأداة [bundletool](https://developer.android.com/tools/bundletool?ref=arabroot.io) بالإصدار 1.18.3، والجهاز محاكي Android 15\. والملف `sign.sh` في مجلد التجربة ينفذ الخطوات بالترتيب، ونعرض هنا كل خطوة مع مخرجها كما ظهر.

⚠️

كلمة المرور `lab-pass-123` في الأوامر للتجربة فقط. في مشروع حقيقي لا تكتب كلمة مرور ملف المفاتيح في سكربت ولا في ملف داخل المستودع، وإنما تقرؤها من متغير بيئة يملؤه نظام البناء من خزنة أسرار Secrets Vault، وتحتفظ بنسخة من ملف المفاتيح نفسه في مكانين منفصلين على الأقل.

### إنشاء مفتاح الرفع Upload Key

نبدأ بمفتاح الرفع بصيغة PKCS12، وهي الصيغة القياسية التي يستخدمها `keytool` اليوم، ومعه مفتاح ثان من نوع EC سوف نستخدمه لاحقاً في تجربة التدوير:

```bash
keytool -genkeypair -keystore upload.p12 -storetype PKCS12 \
  -storepass lab-pass-123 -keypass lab-pass-123 \
  -alias upload -keyalg RSA -keysize 3072 -validity 10000 \
  -dname "CN=Orders Upload, O=Example"
keytool -genkeypair -keystore new.p12 -storetype PKCS12 \
  -storepass lab-pass-123 -keypass lab-pass-123 \
  -alias new -keyalg EC -groupname secp256r1 -validity 10000 \
  -dname "CN=Orders 2027, O=Example"
keytool -list -v -keystore upload.p12 -storepass lab-pass-123
```

والمخرج سوف يكون كما يلي:

```text
Generating 3,072 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 10,000 days
	for: CN=Orders Upload, O=Example
Generating 256 bit EC (secp256r1) key pair and self-signed certificate (SHA384withECDSA) with a validity of 10,000 days
	for: CN=Orders 2027, O=Example
Alias name: upload
Valid from: Fri Oct 30 22:34:16 AST 2026 until: Tue Mar 17 22:34:16 AST 2054
	 SHA256: 2A:16:1A:34:0B:E0:59:FE:FF:9D:0F:C8:98:65:F2:EA:C8:A9:95:8A:A9:25:9C:86:BC:30:BD:9C:F0:3E:51:B3
Signature algorithm name: SHA384withRSA
Subject Public Key Algorithm: 3072-bit RSA key
```

في المخرج أعلاه لاحظ التالي:

- الشهادة موقعة ذاتياً Self-signed، ولا توجد جهة إصدار، وهذا طبيعي في Android كما ذكرنا.
- مدة الصلاحية 10,000 يوم، أي حتى سنة 2054\. وتوثيق Android ينصح بصلاحية 25 سنة أو أكثر لمفتاح التطبيق، والسبب كما يقول أن المستخدمين لا يستطيعون الترقية إلى الإصدارات الجديدة دون عوائق بعد انتهاء صلاحية المفتاح، وأنت لا تريد أن ينتهي مفتاحك قبل أن ينتهي تطبيقك.
- السطر `SHA256` هو بصمة الشهادة Fingerprint، وهي الرقم الذي ستحتاجه في أماكن كثيرة: في ملف `assetlinks.json` للروابط العميقة، وفي إعدادات Firebase وخرائط Google، وعند طلب إعادة تعيين مفتاح الرفع. ولاحظ أن بصمة مفتاح الرفع ليست بصمة مفتاح التطبيق الذي تحتفظ به Google، فخدمات مثل الروابط العميقة تحتاج بصمة مفتاح التطبيق من صفحة App Signing في Play Console، وهذا خطأ شائع يجعل الرابط يعمل في نسخة الاختبار ولا يعمل في نسخة المتجر.

### ماذا فعل R8 بحجم التطبيق؟ تقليص الكود Code Shrinking

قبل التوقيع ننظر إلى ما سوف نوقعه. بنينا التطبيق نفسه مرتين: مرة بإعدادات الإصدار مع [تقليص الكود والموارد](https://developer.android.com/build/shrink-code?ref=arabroot.io) عبر R8، ومرة بنوع بناء أضفناه اسمه `noR8` يرث كل إعدادات الإصدار ولكن يوقف التقليص:

```kotlin
buildTypes {
    release {
        isMinifyEnabled = true
        isShrinkResources = true
        proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
    }
    create("noR8") {
        initWith(getByName("release"))
        isMinifyEnabled = false
        isShrinkResources = false
    }
}
```

والنتيجة:

```text
-rw-r--r-- 1 wajdy 197609 8196328 Oct 30 22:26 noR8-unsigned.apk
-rw-r--r-- 1 wajdy 197609  927869 Oct 30 22:26 v1-unsigned.apk
-- noR8
 14078768  1981-01-01 01:01   classes.dex
  7959232  1981-01-01 01:01   classes2.dex
   472076  1981-01-01 01:01   resources.arsc
-- v1
  1457828  1981-01-01 01:01   classes.dex
    69156  1981-01-01 01:01   resources.arsc
```

الملف دون R8 حجمه 8,196,328 بايت، ومعه 927,869 بايت، أي أقل من ثمن الحجم. والسبب واضح في الأسطر التالية: الكود دون تقليص يحتاج ملفي dex بحجم 14 MB و8 MB تقريباً قبل الضغط، لأن مكتبات Compose وMaterial 3 تحمل آلاف الكلاسات والدوال التي لا يستخدم تطبيقنا الصغير إلا جزءاً منها، وR8 يتتبع ما يصل إليه الكود فعلاً من نقطة البداية ويحذف الباقي، ثم يختصر الأسماء ويدمج الدوال الصغيرة. وكذلك ملف الموارد `resources.arsc` نزل من 472,076 بايت إلى 69,156 لأن تقليص الموارد حذف ما لم يشر إليه الكود.

وهل هذا مجاني؟ لا، وهناك ثمنان. الأول أن أسماء الكلاسات في تقارير الأعطال تصبح مختصرة وغير مقروءة، وتحتاج ملف `mapping.txt` من نفس البناء لتعيدها، وقد شرحنا ذلك عملياً في قسم [رفع ملف ال Mapping](https://arabroot.io/articles/%D9%85%D8%AE%D8%AA%D8%A8%D8%B1-%D8%A7%D8%B9%D8%B7%D8%A7%D9%84-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-android-%D9%85%D8%B9-glitchtip/#mapping) من مختبر الأعطال، فاحفظ هذا الملف مع كل إصدار ترفعه. والثاني أن الكود الذي يستخدم الانعكاس Reflection، مثل بعض مكتبات تحويل JSON، قد يفقد كلاسات يحتاجها وقت التشغيل ولا تظهر المشكلة إلا في نسخة الإصدار، ولذلك اختبر نسخة الإصدار نفسها لا نسخة التصحيح قبل الرفع.

### التوقيع والتحقق بأداة apksigner

أولاً نتأكد أن الملف غير الموقع مصفوف بالشكل الصحيح، والأداة [zipalign](https://developer.android.com/tools/zipalign?ref=arabroot.io) تتحقق من أن الملفات غير المضغوطة داخل الأرشيف تبدأ عند حدود محددة، والخيار `-P 16` يطلب أن تكون المكتبات الأصلية على حدود 16 KB، وسوف نعود إليه:

```bash
zipalign -c -P 16 4 app-release-unsigned.apk && echo "zipalign: aligned"
apksigner sign --ks upload.p12 --ks-pass pass:lab-pass-123 \
  --out orders-1.apk app-release-unsigned.apk
apksigner verify -v --print-certs orders-1.apk
```

والمخرج سوف يكون كما يلي:

```text
zipalign: aligned
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme v3.2): false
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
V3.0 Signer: certificate DN: CN=Orders Upload, O=Example
V3.0 Signer: certificate SHA-256 digest: 2a161a340be059feff9d0fc89865f2eac8a9958aa9259c86bc30bd9cf03e51b3
V3.0 Signer: key algorithm: RSA
V3.0 Signer: key size (bits): 3072
```

في المخرج أعلاه لاحظ التالي:

- الترتيب مهم: `zipalign` قبل `apksigner` دائماً، لأن أي تعديل على الملف بعد التوقيع يفسده كما سنرى، والأداة `apksigner` نفسها تحافظ على المحاذاة.
- `v1 scheme: false` ليس خطأ، فالأداة [apksigner](https://developer.android.com/tools/apksigner?ref=arabroot.io) تقرأ `minSdk` من الملف، وهو 26 في تطبيقنا، وكل الأجهزة من Android 7.0 فما فوق تفهم v2، فلا داعي للمخطط القديم. ولو كان `minSdk` أقل من 24 لأضافته الأداة تلقائياً.
- v2 وv3 كلاهما `true`، والجهاز يختار أحدث مخطط يفهمه.
- البصمة `2a161a34…` هي نفسها التي رأيناها في `keytool`، مكتوبة بحروف صغيرة ودون نقطتين.

### ماذا يحدث إذا تغير ملف بعد التوقيع Tampering؟

الآن نجرب ما يحدث عندما يعدل أحد الملف بعد توقيعه، سواء كان مهاجماً يريد حقن كود، أو أداة بناء تضيف ملفاً بعد خطوة التوقيع بالخطأ. نضيف ملفاً نصياً واحداً من سطر واحد إلى نسخة من الملف الموقع ثم نتحقق:

```bash
cp orders-1.apk tampered.apk
python -I -c "import zipfile; z=zipfile.ZipFile('tampered.apk','a'); z.writestr('res/extra.txt','x'); z.close()"
apksigner verify tampered.apk
```

```text
DOES NOT VERIFY
ERROR: JAR signer UPLOAD.RSA: JAR signature META-INF/UPLOAD.SF indicates the APK is signed using APK Signature Scheme v2 but no such signature was found. Signature stripped?
ERROR: JAR signer UPLOAD.RSA: JAR signature META-INF/UPLOAD.SF indicates the APK is signed using APK Signature Scheme v3 but no such signature was found. Signature stripped?
```

وهذه نتيجة مثيرة أكثر مما توقعنا. فعندما أعادت مكتبة Python كتابة الفهرس المركزي لتضيف الملف، أزالت كتلة التوقيع APK Signing Block بالكامل لأنها لا تعرفها، فلم يبق في الملف إلا ملفات `META-INF` التي وضعتها `apksigner` للتوافق، وهذه الملفات نفسها تقول إن الملف موقع بالمخطط v2 وv3، فتسأل الأداة: أين ذهب التوقيع؟ والعبارة `Signature stripped?` هي الحماية المقصودة من هجوم حذف التوقيع الأحدث والاكتفاء بالأقدم الأضعف. والدرس العملي أن أي خطوة في خط البناء Pipeline تعدل الملف، مثل إضافة ملف إعداد أو أداة حماية تعيد تغليفه، يجب أن تأتي قبل التوقيع لا بعده.

### التحديث Update بمفتاح مختلف وبرقم إصدار versionCode أقدم

هذه هي الحالة التي حدثت لفريقنا يوم الخميس: نسخ الاختبار موقعة بمفتاح التصحيح، والنسخة الجديدة بمفتاح الرفع. نثبت أولاً ملفاً غير موقع، ثم نسخة موقعة بمفتاح التصحيح، ثم نحاول تثبيت نسخة مفتاح الرفع فوقها، ثم نحذف التطبيق ونثبتها من جديد:

```bash
adb install noR8-unsigned.apk
apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android \
  --out orders-debugkey.apk app-release-unsigned.apk
adb install orders-debugkey.apk
adb install -r orders-1.apk
adb uninstall io.example.orders.native
adb install orders-1.apk
```

```text
adb: failed to install noR8-unsigned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl1416264317.tmp/base.apk: Attempt to get length of null array]
Success
adb: failed to install orders-1.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package io.example.orders.native signatures do not match newer version; ignoring!]
Success
```

السطر الأول يبين أن Android لا يقبل ملفاً دون توقيع إطلاقاً، حتى للتجربة، وهذا سبب وجود مفتاح التصحيح أصلاً: Android Studio يوقع به كل بناء تصحيح تلقائياً. والسطر الثالث هو المهم: `INSTALL_FAILED_UPDATE_INCOMPATIBLE` مع العبارة `signatures do not match`، فالجهاز رفض التحديث لأن الشهادة مختلفة، ولم يعرض على المستخدم خياراً. والحل الوحيد كان حذف التطبيق، وهذا يعني حذف كل بياناته. ولذلك إذا وزعت نسخ اختبار موقعة بمفتاح التصحيح على عملاء حقيقيين، فكل واحد منهم سيضطر لحذف التطبيق وبياناته عندما ينتقل إلى نسخة المتجر، فوزع نسخ الاختبار من مسارات الاختبار في المتجر نفسه بدلاً من ذلك.

ثم نجرب رقم الإصدار `versionCode`. بنينا نسخة برقم 2 ثم حاولنا الرجوع إلى رقم 1:

```text
Success
Success
    versionCode=2 minSdk=26 targetSdk=36
adb: failed to install orders-1.apk: Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 1 is older than current 2]
```

تثبيت نفس الرقم 1 فوق نفسه نجح، ثم الرقم 2 نجح، أما الرجوع إلى 1 فرفضه الجهاز بالرسالة `INSTALL_FAILED_VERSION_DOWNGRADE`. والمتجر أشد من الجهاز، فهو لا يقبل أصلاً رفع حزمة برقم سبق استخدامه، كما تشرح صفحة [إصدارات التطبيق](https://developer.android.com/studio/publish/versioning?ref=arabroot.io). وهذا يعني أن الرجوع عن إصدار سيئ في المتجر لا يكون بإعادة رفع النسخة القديمة، وإنما ببناء النسخة القديمة من جديد برقم أعلى، أو بإيقاف الإطلاق التدريجي قبل أن يصل إلى الجميع، وقد شرحنا ذلك في قسم [الإطلاق التدريجي Staged Rollout](https://arabroot.io/articles/%D8%AA%D9%82%D8%A7%D8%B1%D9%8A%D8%B1-%D8%A7%D9%84%D8%A7%D8%B9%D8%B7%D8%A7%D9%84-crash-reporting-%D9%81%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84/#staged-rollout) من مقال تقارير الأعطال.

### تدوير المفتاح Key Rotation بسلسلة النسب Lineage

لنفرض الآن أن الفريق قرر بعد سنة أن ينتقل إلى مفتاح جديد، ربما لأن الموظف الذي أنشأ المفتاح القديم غادر، أو لأن الشركة تريد مفتاحاً من نوع EC أقصر وأحدث. لو وقعنا الإصدار 3 بالمفتاح الجديد مباشرة فسوف يحدث ما رأيناه قبل قليل. والطريقة الصحيحة أن نصنع سلسلة نسب Lineage يوقع فيها المفتاح القديم على الجديد، ثم نوقع الإصدار بالمفتاحين:

```bash
apksigner rotate --out lineage.bin \
  --old-signer --ks upload.p12 --ks-pass pass:lab-pass-123 \
  --new-signer --ks new.p12 --ks-pass pass:lab-pass-123
apksigner lineage --in lineage.bin --print-certs
# the new key alone
apksigner sign --ks new.p12 --ks-pass pass:lab-pass-123 --out orders-3-nolineage.apk v3-unsigned.apk
adb install -r orders-3-nolineage.apk
# both keys and the lineage
apksigner sign --ks upload.p12 --ks-pass pass:lab-pass-123 \
  --next-signer --ks new.p12 --ks-pass pass:lab-pass-123 \
  --lineage lineage.bin --out orders-3.apk v3-unsigned.apk
apksigner verify -v --print-certs orders-3.apk
adb install -r orders-3.apk
```

```text
Signer #1 in lineage certificate DN: CN=Orders Upload, O=Example
Signer #1 in lineage certificate SHA-256 digest: 2a161a340be059feff9d0fc89865f2eac8a9958aa9259c86bc30bd9cf03e51b3
Signer #2 in lineage certificate DN: CN=Orders 2027, O=Example
Signer #2 in lineage certificate SHA-256 digest: 6a0255f9c473394063a4327bef8dc1b81247b40e2235f893f4e9639aaea0eec7
-- v3 signed with the NEW key only, no lineage
adb: failed to install orders-3-nolineage.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package io.example.orders.native signatures do not match newer version; ignoring!]
-- v3 signed with both keys and the lineage
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate SHA-256 digest: 6a0255f9c473394063a4327bef8dc1b81247b40e2235f893f4e9639aaea0eec7
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate SHA-256 digest: 2a161a340be059feff9d0fc89865f2eac8a9958aa9259c86bc30bd9cf03e51b3
Success
    versionCode=3 minSdk=26 targetSdk=36
    signatures=PackageSignatures{ee88945 version:4, signatures:[1a941d14], past signatures:[e55f6d02 flags: 17, 1a941d14 flags: 17]}
```

في المخرج أعلاه لاحظ التالي:

- السلسلة فيها موقعان بالترتيب: القديم ثم الجديد، وهذا ما يسميه التوثيق إثبات التدوير.
- الإصدار الموقع بالمفتاح الجديد وحده رفضه الجهاز بنفس رسالة اختلاف التوقيع، فالمفتاح الجديد وحده لا يعني شيئاً للجهاز.
- الإصدار الموقع مع السلسلة يحمل كتلتين: كتلة v3.1 بالمفتاح الجديد للأجهزة من SDK 33 فما فوق، وكتلة v3.0 بالمفتاح القديم للأجهزة من 24 إلى 32\. وهذا السلوك هو ما يصفه توثيق v3.1: الأجهزة من Android 13 تستخدم المفتاح الجديد، والأقدم تتجاهله وتبقى على القديم، لأن التوثيق نفسه لا ينصح بالتدوير على Android 12 وما قبله.
- المحاكي يعمل بنظام Android 15، أي SDK 35، فقبل التحديث، وظهر في `dumpsys` أن للتطبيق توقيعاً حالياً وتوقيعات سابقة past signatures تضم المفتاحين.

![المفتاح القديم RSA يوقع على المفتاح الجديد EC بأمر apksigner rotate، والإصدار 3 الموقع بالمفتاحين مع السلسلة قبله Android 15 تحديثاً، أما الموقع بالمفتاح الجديد وحده فرفضه الجهاز](https://arabroot.io/content/images/2026/10/mobile-store-publishing-04-key-rotation.webp)

التدوير يحتاج أن يوقع المفتاح القديم على الجديد، والمفتاح الجديد وحده لا يكفي

ولاحظ أن هذا التدوير يخص مفتاح التطبيق الذي يراه الجوال، فإذا كنت تستخدم Play App Signing فمفتاح التطبيق عند Google، والتدوير يتم من Play Console لا من جهازك، أما مفتاح الرفع فتغييره أسهل بكثير لأنه مجرد طلب إعادة تعيين. والتجربة هنا تبين لك ما يحدث داخل الملف عندما تطلب ذلك من المتجر.

### حزمة التطبيق App Bundle وما يصل إلى كل جهاز

Google Play لا يقبل من التطبيقات الجديدة ملف APK، وإنما حزمة AAB نوقعها بمفتاح الرفع بالأداة `jarsigner`، ثم نستخدم `bundletool` لنرى ما سيفعله المتجر بها. وعرفنا للأداة جهازاً بمعالج arm64 ولغة عربية وكثافة شاشة 420 وSDK 35:

```bash
jarsigner -keystore upload.p12 -storepass lab-pass-123 \
  -signedjar orders.aab app-release.aab upload
cat > pixel.json <<'EOF'
{ "supportedAbis": ["arm64-v8a"], "supportedLocales": ["ar"], "screenDensity": 420, "sdkVersion": 35 }
EOF
java -jar bundletool.jar build-apks --bundle=orders.aab --output=orders.apks \
  --ks=upload.p12 --ks-pass=pass:lab-pass-123 --ks-key-alias=upload --key-pass=pass:lab-pass-123
java -jar bundletool.jar get-size total --apks=orders.apks
java -jar bundletool.jar get-size total --apks=orders.apks --device-spec=pixel.json
java -jar bundletool.jar extract-apks --apks=orders.apks --device-spec=pixel.json --output-dir=pixel
```

```text
-rw-r--r-- 1 wajdy 197609 2247031 Oct 30 22:26 app-release.aab
jar signed.
Warning:
The signer's certificate is self-signed.
MIN,MAX
776254,782440
MIN,MAX
781753,781753
-rw-r--r-- 1 wajdy 197609   12435 Oct 30 22:35 base-ar_3.apk
-rw-r--r-- 1 wajdy 197609   37039 Oct 30 22:35 base-arm64_v8a_3.apk
-rw-r--r-- 1 wajdy 197609 1526473 Oct 30 22:35 base-master_3.apk
total apks: 238
```

وهذا أهم مخرج في التجربة كلها لمن يهتم بحجم التطبيق، فلنقرأه سطراً سطراً:

- الحزمة نفسها 2,247,031 بايت، وهي أكبر من ملف APK الذي وقعناه سابقاً، لأنها تحمل كل شيء: المكتبات الأصلية لأربعة معالجات، والموارد لكل الكثافات، والترجمات لكل اللغات.
- تحذير `self-signed` من `jarsigner` طبيعي، وسببه نفس ما ذكرناه: لا توجد جهة إصدار في توقيع Android.
- السطر `MIN,MAX` الأول هو حجم التنزيل المقدر لأي جهاز ممكن، بين 776,254 و782,440 بايت، والثاني لجهازنا المحدد 781,753 بايت. وهذا الرقم حجم التنزيل المضغوط Download Size، وليس الحجم على القرص.
- الأداة صنعت 238 ملف APK داخل ملف `orders.apks`: ملف أساسي master، وملف لكل معالج ABI، وملف لكل لغة من نحو 85 لغة، وبعضها بأكثر من نسخة لنطاقات مختلفة من إصدارات Android، ولذلك تحمل الأسماء لاحقة مثل `_3`.
- الجهاز العربي بمعالج arm64 أخذ ثلاثة ملفات فقط: الأساسي بحجم 1.5 MB، وملف اللغة العربية 12 KB، وملف المعالج 37 KB. وعندما فتحنا ملف المعالج وجدنا فيه مكتبة واحدة هي `libandroidx.graphics.path.so` بحجم 10,096 بايت تأتي مع Compose، والباقي من حجم الملف هو ملف Manifest الخاص به وبيانات التوقيع والمحاذاة.

![حزمة AAB بحجم 2,247,031 بايت تمر على bundletool فتصبح 238 ملف APK، وجوال arm64 بالعربية يأخذ ثلاثة منها فقط: الأساسي 1,526,473 وملف المعالج 37,039 وملف العربية 12,435 بايت، وحجم التنزيل المقدر 781,753 بايت](https://arabroot.io/content/images/2026/10/mobile-store-publishing-05-bundle-splits.webp)

الحزمة الواحدة تقطع إلى ملفات، وكل جهاز يأخذ ما يناسبه فقط

وماذا يحدث لو لم تستخدم الحزمة ووزعت ملف APK واحداً؟ سوف يحمل كل مستخدم مكتبات أربعة معالجات وترجمات كل اللغات. وفي تطبيقنا الصغير الفرق صغير، أما في تطبيقات Flutter وReact Native فالمكتبات الأصلية هي أغلب الحجم، وقد رأينا في مقال اختيار التقنية أن ملف APK الشامل لنسخة React Native كان 55 MB لأنه يحمل مكتبات أربعة معالجات، بينما كل جوال يحتاج مكتبات معالج واحد فقط. وإذا كان تطبيقك لا يدعم إلا لغتين، فيمكنك أيضاً تقليل ملفات اللغات من البداية بإعداد [اللغات المدعومة في التطبيق](https://developer.android.com/guide/topics/resources/app-languages?ref=arabroot.io) بدلاً من أن ترث 85 لغة من المكتبات.

### محاذاة الصفحات 16 KB Page Size في مكتبات Flutter وReact Native

هناك متطلب جديد يمس كل تطبيق فيه كود أصلي Native Code، وهذا يشمل كل تطبيقات Flutter وReact Native تقريباً: بعض أجهزة Android الحديثة تعمل بحجم صفحة ذاكرة 16 KB بدلاً من 4 KB، والمكتبات الأصلية يجب أن تكون مبنية ومصفوفة لذلك. وتقول صفحة [دعم أحجام الصفحات 16 KB](https://developer.android.com/guide/practices/page-sizes?ref=arabroot.io) إن كل التطبيقات التي تستهدف Android 15 فما فوق يجب أن تدعم صفحات 16 KB على الأجهزة 64-bit في Google Play، وإنه بدءاً من 1 فبراير 2027 لن تتمكن من نشر تحديثات لا تدعمها. فحصنا ملفي الإصدار من نسختي Flutter وReact Native لنفس التطبيق:

```bash
zipalign -c -P 16 -v 4 app-arm64-v8a-release.apk | grep -E "\.so|Verification"
```

```text
  245760 lib/arm64-v8a/libapp.so (OK)
 3538944 lib/arm64-v8a/libflutter.so (OK)
Verification successful
```

والملف الأول من Flutter فيه مكتبتان: `libflutter.so` وهي المحرك نفسه، و`libapp.so` وهي كود Dart الخاص بتطبيقنا بعد ترجمته مسبقاً. أما ملف React Native فمكتباته أكثر:

```bash
zipalign -c -P 16 -v 4 app-release.apk | grep -E "arm64.*\.so|Verification"
```

```text
 4210688 lib/arm64-v8a/libappmodules.so (OK)
 4358144 lib/arm64-v8a/libc++_shared.so (OK)
 5652480 lib/arm64-v8a/libfbjni.so (OK)
 5832704 lib/arm64-v8a/libhermestooling.so (OK)
 5980160 lib/arm64-v8a/libhermesvm.so (OK)
 8699904 lib/arm64-v8a/libimagepipeline.so (OK)
 8716288 lib/arm64-v8a/libjsi.so (OK)
 9142272 lib/arm64-v8a/libnative-filters.so (OK)
 9420800 lib/arm64-v8a/libnative-imagetranscoder.so (OK)
10010624 lib/arm64-v8a/libreact_codegen_safeareacontext.so (OK)
10174464 lib/arm64-v8a/libreactnative.so (OK)
Verification successful
```

الرقم في بداية كل سطر هو موضع المكتبة داخل الملف، وكلها مضاعفات 16384، أي مصفوفة على حدود 16 KB، والأداة كتبت `OK` لكل مكتبة. وهذا يعني أن الإصدارات الحديثة من الإطارين تبني بالشكل الصحيح. ولكن هذا الفحص يتحقق من الموضع داخل الملف فقط، أما المكتبة نفسها فيجب أن تكون مبنية بمحاذاة 16 KB لأقسامها، وهذا يعتمد على إصدار NDK الذي بنيت به، والمشكلة الحقيقية تظهر عادة في مكتبة قديمة من طرف ثالث لم يحدثها صاحبها، فافحص الملف النهائي بعد إضافة كل مكتبة أصلية جديدة، وPlay Console يعرض تحذيراً عند الرفع إذا وجد مكتبة غير متوافقة.

### الصلاحيات Permissions التي لم تطلبها: الملف المدمج Merged Manifest

آخر فحص قبل الرفع هو ما يقرؤه المتجر من الملف. أخذنا تطبيق دورة الحياة من هذه المرحلة، وملف `AndroidManifest.xml` الذي كتبناه فيه لا يطلب أي صلاحية، ثم قرأنا الصلاحيات من الملف النهائي بالأداة `aapt2`:

```bash
grep -c "uses-permission" app/src/main/AndroidManifest.xml
aapt2 dump permissions app-debug.apk
```

```text
0
package: io.example.life
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.FOREGROUND_SERVICE'
permission: io.example.life.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
uses-permission: name='io.example.life.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'
```

صفر صلاحيات في ملفنا، وأربع صلاحيات نظام في الملف النهائي. والسبب أن أداة البناء تدمج ملف Manifest الخاص بكل مكتبة في ملفك، ومكتبة WorkManager تحتاج هذه الصلاحيات لتعمل: إبقاء المعالج مستيقظاً أثناء المهمة، ومعرفة حالة الشبكة لقيد الاتصال، وإعادة جدولة المهام بعد إعادة تشغيل الجهاز، وتشغيل خدمة في المقدمة للمهام الطويلة. وهذه الصلاحيات ليست خطيرة، ولكن المراجع والمستخدم يرونها، ونموذج أمان البيانات في Play Console يجب أن يطابق سلوك التطبيق الفعلي بما فيه المكتبات. والقاعدة: اقرأ الصلاحيات من الملف النهائي لا من ملفك، واحذف ما لا تحتاجه بالوسم `tools:node="remove"` بعد أن تتأكد أن المكتبة لا تحتاجه فعلاً.

وبنفس الأداة نرى ما يعرضه المتجر عن التطبيق:

```text
package: name='io.example.orders.native' versionCode='1' versionName='1.0' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
targetSdkVersion:'36'
application-label:'طلباتي'
(+ 85 application-label-<locale> lines, one per locale the libraries ship resources for)
supports-screens: 'small' 'normal' 'large' 'xlarge'
native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64'
```

لاحظ `targetSdkVersion:'36'`، وهو الرقم الذي يحدد هل يقبل المتجر الرفع أصلاً كما سنرى في القسم التالي، و`compileSdkVersion='37'` الذي اضطررنا إليه لأن إصدار Compose الحالي يطلب ذلك. ولاحظ السطر الذي اختصرناه: 85 اسماً مترجماً للتطبيق، كلها `طلباتي`، لأن المكتبات تحمل ترجمات لهذه اللغات، وهذا ما رأيناه في ملفات اللغات داخل الحزمة. والسطر الأخير يبين أن الملف الشامل يحمل مكتبات أربعة معالجات، وهذا ما تحله الحزمة.

## ما الذي تطلبه Google Play في 2026؟ متطلبات النشر Publishing Requirements

بعد أن أصبح لدى فريقنا حزمة موقعة وصحيحة، يأتي دور Play Console. وقبل الرفع هناك أربعة متطلبات تمنع النشر إذا لم تستوفها، وكلها تتغير كل سنة تقريباً، ولذلك نقتبس كل واحد منها من صفحته الرسمية بتاريخ قراءتنا لها في أكتوبر 2026، وعليك أن تراجع الصفحة نفسها قبل أن ترفع.

### مستوى API المستهدف Target API Level

تقول صفحة [متطلبات مستوى API المستهدف](https://developer.android.com/google/play/requirements/target-sdk?ref=arabroot.io) إنه بدءاً من 31 أغسطس 2026 يجب أن تستهدف التطبيقات الجديدة وتحديثات التطبيقات Android 16، أي API 36 أو أعلى، لكي تقبل في Google Play، مع استثناءات لتطبيقات Wear OS وAndroid Automotive التي تكفيها API 35، وتطبيقات التلفاز وAndroid XR التي تكفيها API 34\. أما التطبيقات الموجودة التي لا تحدث، فيجب أن تستهدف API 35 على الأقل لتبقى متاحة للمستخدمين الجدد على الأجهزة التي تعمل بنظام أحدث من مستواها المستهدف.

ولماذا تفرض Google ذلك؟ لأن رقم `targetSdkVersion` هو الذي يقول للنظام أي قواعد سلوك يطبقها على التطبيق. فالتطبيق الذي يستهدف إصداراً قديماً يعمل على جوال حديث بقواعد الإصدار القديم في أشياء مثل الصلاحيات وعمل الخلفية، وهذا ما تريد المنصة أن توقفه. والأثر العملي على فريقنا أن رفع `targetSdk` ليس تعديل رقم فقط، فكل رفع يغير سلوكاً، مثل قيود الخدمات في الخلفية التي جربناها في مقال دورة الحياة من هذه المرحلة، ولذلك اختبر التطبيق بعد رفع الرقم لا قبله. ولاحظ الفرق بين الأرقام الثلاثة: `minSdk` أقدم نظام يعمل عليه التطبيق، و`targetSdk` النظام الذي اختبرت سلوكه، و`compileSdk` الواجهات التي يرى الكود أنها موجودة، وفي تجربتنا كانت 26 و36 و37 بالترتيب.

### الاختبار المغلق Closed Testing للحسابات الشخصية

هذا المتطلب يفاجئ المطورين المستقلين والفرق الصغيرة أكثر من غيره. تقول صفحة [متطلبات الاختبار للحسابات الشخصية](https://support.google.com/googleplay/android-developer/answer/14151465?ref=arabroot.io) إن المطورين الذين أنشأوا حسابات شخصية بعد 13 نوفمبر 2023 يجب أن يشغلوا اختباراً مغلقاً لتطبيقهم مع 12 مختبراً على الأقل مشتركين فيه باستمرار لمدة 14 يوماً على الأقل، وبعدها فقط يمكنهم التقدم بطلب الوصول إلى الإنتاج Production من لوحة Play Console، ويجيبون في الطلب على أسئلة عن التطبيق وطريقة اختباره.

وهذا يعني لفريقنا الذي أنشأ حساباً شخصياً قبل أسبوع أن موعد الأحد مستحيل على Android مهما كان التطبيق جاهزاً، فأقرب موعد بعد أسبوعين من بدء الاختبار المغلق، ثم وقت مراجعة طلب الإنتاج. والحل الذي كان يجب أن يحدث من البداية واحد من اثنين: حساب منظمة Organization Account، لأن هذا المتطلب مكتوب للحسابات الشخصية كما تقول الصفحة، مع أن حساب المنظمة يحتاج وثائق الشركة ووقتاً للتحقق منها، أو بدء الاختبار المغلق مع أول نسخة قابلة للاستخدام لا مع النسخة الأخيرة.

### قسم أمان البيانات Data Safety وحذف الحساب Account Deletion

كل تطبيق في Google Play يجب أن يملأ نموذج [أمان البيانات Data Safety](https://support.google.com/googleplay/android-developer/answer/10787469?ref=arabroot.io)، وهو ما يظهر للمستخدم في صفحة التطبيق: ما البيانات التي يجمعها، وهل يشاركها، وهل هي مشفرة أثناء النقل، وهل يمكن طلب حذفها. والنموذج يجب أن يشمل ما تجمعه المكتبات داخل تطبيقك، مثل أداة تقارير الأعطال التي تجمع معرف الجهاز، أو مكتبة التحليلات، وهذا سبب فحصنا للملف المدمج قبل قليل.

وإذا كان تطبيقك يسمح بإنشاء حساب، فصفحة [متطلبات حذف الحساب](https://support.google.com/googleplay/android-developer/answer/13327111?ref=arabroot.io) تطلب أمرين: مساراً داخل التطبيق يحذف به المستخدم حسابه والبيانات المرتبطة به، ورابط ويب يطلب منه حذف الحساب والبيانات دون أن يحتاج إلى تثبيت التطبيق. وتطبيق الطلبات فيه حساب وعنوان توصيل وسجل طلبات، ولذلك يحتاج الفريق صفحة ويب صغيرة لطلب الحذف وزراً في شاشة الإعدادات، ويحتاج قبل ذلك قراراً قانونياً: ما الذي يحذف فعلاً وما الذي تحتفظ به الشركة لأغراض محاسبية، وهذا ما يجب أن يكتب في سياسة الخصوصية.

### التحقق من هوية المطور Developer Verification

وهناك تغيير أكبر بدأ هذه السنة ويمس التوزيع خارج Google Play أيضاً. تقول صفحة [التحقق من هوية مطوري Android](https://developer.android.com/developer-verification?ref=arabroot.io) إن Android أطلق برنامجاً يتحقق من هوية المطورين لحماية المستخدمين من البرمجيات الخبيثة والاحتيال، وإن Google Play يسجل 99% من التطبيقات تلقائياً، والجدول الزمني في الصفحة كما يلي: في أغسطس 2026 أطلقت الواجهات البرمجية وحسابات التوزيع المحدود ومسار المستخدمين المتقدمين، وفي 30 سبتمبر 2026 بدأت الحماية لكل المستخدمين الذين يثبتون تطبيقات من المتاجر المشاركة في البرازيل وإندونيسيا وسنغافورة وتايلاند على الأجهزة المعتمدة بنظام Android 7 فما فوق، وفي 2027 تتوسع لكل التطبيقات على الأجهزة المعتمدة في العالم. وإذا كان تطبيقك يوزع على Google Play فقط فغالباً لا تحتاج شيئاً إضافياً، أما إذا كان الفريق يوزع ملف APK مباشرة لعملاء من الشركات أو من موقعه، فعليه أن يسجل في Android Developer Console قبل أن يصل التطبيق إلى أجهزة لا تثبت إلا تطبيقات المطورين الموثقين.

### المراجعة في Google Play Review والإطلاق التدريجي

لا تفترض أن مراجعة Google تنتهي في ساعات لأن تحديثاتك السابقة انتهت كذلك، فتقول صفحة [حالة النشر](https://support.google.com/googleplay/android-developer/answer/9859751?ref=arabroot.io) إن بعض التطبيقات قد تخضع لمراجعات ممتدة قد تصل إلى سبعة أيام أو أكثر في حالات استثنائية. وبعد القبول لا تنشر الإصدار لكل المستخدمين مرة واحدة، وإنما بالإطلاق التدريجي Staged Rollout الذي تشرحه [صفحة الإطلاق التدريجي](https://support.google.com/googleplay/android-developer/answer/6346149?ref=arabroot.io)، فتبدأ بنسبة صغيرة وتراقب الأعطال ثم ترفع النسبة، وتوقف الإطلاق إذا ظهرت مشكلة. ولن نكرر هنا كيف تقرأ أرقام الأعطال أثناء الإطلاق، فقد شرحناه في مقال [تقارير الأعطال](https://arabroot.io/articles/%D8%AA%D9%82%D8%A7%D8%B1%D9%8A%D8%B1-%D8%A7%D9%84%D8%A7%D8%B9%D8%B7%D8%A7%D9%84-crash-reporting-%D9%81%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84/#staged-rollout).

## مراجعة Apple App Review: ما الذي تقرؤه الإرشادات Guidelines فعلاً؟

في Apple يراجع كل إصدار إنسان، والمرجع هو [إرشادات مراجعة App Store](https://developer.apple.com/app-store/review/guidelines/?ref=arabroot.io)، وقد كان آخر تحديث لها حين قرأناها في 16 أكتوبر 2026\. والإرشادات مقسمة إلى خمسة أقسام: السلامة Safety، والأداء Performance، والأعمال Business، والتصميم Design، والقانوني Legal. وقبل أن نقرأ أهمها لفريقنا، لننظر إلى الأرقام لنعرف أين يسقط أغلب الناس.

### أرقام المراجعة في تقرير الشفافية Transparency Report

تنشر Apple كل سنة [تقرير شفافية App Store](https://www.apple.com/legal/more-resources/docs/2024-App-Store-Transparency-Report.pdf?ref=arabroot.io)، وفي تقرير 2024، وهو آخر تقرير وجدناه منشوراً، راجعت Apple 7,771,599 طلباً، ورفضت منها 1,931,400، أي نحو الربع، ثم قبلت 295,109 طلباً بعد رفضها، أي بعد أن أصلحها أصحابها وأعادوا إرسالها. وتوزيع الرفض على الأقسام يقول الكثير: قسم الأداء Performance وحده 1,235,471، ثم القانوني 445,696، ثم التصميم 378,300، ثم الأعمال 209,845، ثم السلامة 116,105\. ولاحظ أن مجموع الأقسام أكبر من عدد الطلبات المرفوضة، لأن التقرير ينبه إلى أن الطلب الواحد قد يرفض لأكثر من إرشاد.

![أعمدة أفقية لعدد مرات الرفض في كل قسم من إرشادات Apple في 2024: الأداء 1,235,471 ثم القانوني 445,696 ثم التصميم 378,300 ثم الأعمال 209,845 ثم السلامة 116,105 ثم أخرى 23,659، وفوقها 7,771,599 طلب مراجعة و1,931,400 مرفوض و295,109 قبلت بعد الرفض](https://arabroot.io/content/images/2026/10/mobile-store-publishing-07-apple-rejections.webp)

قسم الأداء، وفيه اكتمال التطبيق، أكبر سبب للرفض عند Apple في 2024

وقسم الأداء يبدأ بالإرشاد 2.1 اكتمال التطبيق App Completeness، وتقول صفحة [App Review](https://developer.apple.com/distribute/app-review/?ref=arabroot.io) نفسها إن أكثر من 40% من المشكلات غير المحلولة في المتوسط مرتبطة بالإرشاد 2.1، الذي يشمل الأعطال والمحتوى المؤقت والمعلومات الناقصة. وتقول الصفحة أيضاً إن فريق المراجعة يراجع عادة 50% على الأقل من الطلبات في أقل من 24 ساعة و90% في أقل من 48 ساعة، فالرفض لا يكلفك أسابيع في الغالب، ولكن كل دورة رفض وإصلاح وإعادة إرسال تأكل يومين أو ثلاثة من موعدك.

### الإرشادات التي توقف التطبيقات الجديدة Guidelines

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

- اكتمال التطبيق 2.1: تطلب الفقرة (a) أن تكون الطلبات «final versions with all necessary metadata and fully functional URLs»، وأن يزال النص المؤقت والمواقع الفارغة، وأن ترسل «demo account info (and turn on your back-end service!)» إذا كان التطبيق يحتاج تسجيل دخول. وهذا بالضبط ما رفض به تطبيق فريقنا في المرة الأولى: المراجع لم يستطع تجاوز شاشة تسجيل الدخول برقم الجوال، لأن التطبيق يرسل رمز تحقق برسالة نصية إلى رقم لا يملكه المراجع. والحل حساب تجريبي برقم ثابت يقبل رمزاً ثابتاً، أو وضع تجريبي داخل التطبيق كما تسمح الفقرة نفسها، وسيرفر الاختبار يجب أن يكون شغالاً طوال فترة المراجعة.
- دقة البيانات الوصفية 2.3: يجب أن تعكس الصور والوصف «the app’s core experience»، والإرشاد 2.3.3 يطلب أن تظهر لقطات الشاشة التطبيق وهو يعمل، لا شاشة الشعار ولا شاشة تسجيل الدخول فقط، والإرشاد 2.3.7 يحدد اسم التطبيق بثلاثين حرفاً ويمنع حشو الكلمات المفتاحية بأسماء تطبيقات أخرى.
- الحد الأدنى من الوظائف 4.2: «Your app should include features, content, and UI that elevate it beyond a repackaged website». والتطبيق الذي يغلف موقع المتجر داخل WebView فقط يرفض غالباً بهذا الإرشاد، وهذا من أهم الأسباب التي تجعل بعض الفرق تختار بناء واجهة أصلية أو متعددة المنصات بدلاً من الغلاف.
- الرسائل المزعجة Spam 4.3: «Don’t create multiple Bundle IDs of the same app». فإذا أراد العميل تطبيقاً لكل فرع أو لكل مدينة، فالإرشاد يطلب تطبيقاً واحداً يختار فيه المستخدم الفرع.
- سياسة الخصوصية 5.1.1(i): «All apps must include a link to their privacy policy in the App Store Connect metadata field and within the app in an easily accessible manner». الرابط مطلوب في مكانين، وصفحة App Review تذكر أن الروابط المعطلة، بما فيها رابط الدعم وسياسة الخصوصية، من أسباب الرفض الشائعة.
- تسجيل الدخول وحذف الحساب 5.1.1(v): «If your app supports account creation, you must also offer account deletion within the app». وتشرح Apple التفاصيل في صفحة [حذف الحساب داخل التطبيق](https://developer.apple.com/support/offering-account-deletion-in-your-app/?ref=arabroot.io). ولاحظ أن Google تطلب الشيء نفسه مع رابط ويب إضافي، فابن الميزة مرة واحدة للمنصتين.
- خدمات تسجيل الدخول 4.8: إذا استخدمت تسجيل دخول من طرف ثالث مثل Google أو Facebook للحساب الأساسي، فيجب أن تقدم خياراً مكافئاً يحصر البيانات بالاسم والبريد، ويسمح بإخفاء البريد، ولا يجمع التفاعلات للإعلان دون موافقة. وتسجيل الدخول برقم الجوال وحده لا يدخل في هذا الإرشاد لأنه ليس خدمة طرف ثالث.
- الشراء داخل التطبيق 3.1.1 و3.1.3(e): فتح ميزات رقمية داخل التطبيق يجب أن يمر بالشراء داخل التطبيق In-App Purchase، أما بيع السلع المادية والخدمات التي تستهلك خارج التطبيق فيجب أن يستخدم طرق دفع أخرى مثل Apple Pay أو البطاقة، وهذه حالة تطبيق الطلبات. والخلط بين الحالتين في الاتجاهين سبب رفض معروف، وقد شرحنا الشراء داخل التطبيق في مقال الروابط العميقة والإشعارات من هذه المرحلة.
- تنزيل الكود 2.5.2: لا يجوز للتطبيق أن ينزل كوداً «which introduces or changes features or functionality of the app». وهذا يمس أدوات التحديث عبر الهواء Over-the-Air في React Native، فتحديث نص أو إصلاح خطأ شيء، وإضافة ميزة جديدة دون مراجعة شيء آخر يخالف الإرشاد.

### الخصوصية Privacy: ملصق البيانات Privacy Label وملف Privacy Manifest

عند Apple طبقتان للخصوصية غير سياسة الخصوصية نفسها. الأولى [تفاصيل الخصوصية في صفحة التطبيق](https://developer.apple.com/app-store/app-privacy-details/?ref=arabroot.io)، وهي أسئلة تجيب عنها في App Store Connect عن البيانات التي تجمعها أنت والمكتبات، وتظهر للمستخدم قبل التنزيل، وهي المقابل المباشر لقسم أمان البيانات في Google Play. والثانية [ملف Privacy Manifest](https://developer.apple.com/documentation/bundleresources/privacy-manifest-files?ref=arabroot.io) باسم `PrivacyInfo.xcprivacy` داخل التطبيق وداخل كل مكتبة، ويسجل كما تقول الوثيقة أنواع البيانات التي يجمعها التطبيق أو المكتبة، والواجهات البرمجية التي تتطلب سبباً Required Reasons APIs مع سبب استخدام كل واحدة. وهنا يظهر الفرق بين المنصتين: في Android نكتشف صلاحيات المكتبات من الملف المدمج كما فعلنا، وفي iOS يجب أن تحمل كل مكتبة ملفها، ويجمع Xcode تقرير الخصوصية Privacy Report من الملفات كلها، فإذا كانت مكتبة قديمة لا تحمل ملفاً فسوف تجد نفسك تشرح استخدامها بدلاً عنها أو تستبدلها.

### متطلبات الأدوات Xcode SDK والحد الأدنى للنظام

وكما في Google، تفرض Apple حداً أدنى لأدوات البناء. تقول صفحة [المتطلبات القادمة](https://developer.apple.com/news/upcoming-requirements/?ref=arabroot.io) إنه منذ 28 أبريل 2026 يجب أن تبنى التطبيقات المرفوعة إلى App Store Connect بإصدار Xcode 26 أو أحدث مع SDK الإصدار 26 من أنظمة Apple، وإنه منذ 9 سبتمبر 2026 يجب أن تستهدف تطبيقات iOS وiPadOS المرفوعة iOS 13 أو أحدث. والأثر على الفرق التي تستخدم Flutter أو React Native أن عليها تحديث الإطار نفسه ليبني مع Xcode الجديد، ولذلك لا تترك إصدار الإطار قديماً لسنة كاملة ثم تكتشف قبل الإطلاق أنه لا يبني مع الأداة المطلوبة.

## قبل الإنتاج Production: TestFlight ومسارات الاختبار Testing Tracks

القاعدة التي تعلمها فريقنا يوم الخميس: لا توزع نسخة اختبار خارج المتجر. في Apple يوجد [TestFlight](https://developer.apple.com/testflight/?ref=arabroot.io) لتوزيع النسخ التجريبية على مختبرين داخليين من فريقك وعلى مختبرين خارجيين بعد مراجعة خفيفة للنسخة التجريبية، وفي Google Play توجد مسارات الاختبار الداخلي Internal والمغلق Closed والمفتوح Open. والفائدة أكبر من مجرد التوزيع: النسخة التي يجربها المختبر هي نفس الحزمة الموقعة بنفس المفتاح والتي ستصل إلى الإنتاج، فلا توجد مفاجأة `INSTALL_FAILED_UPDATE_INCOMPATIBLE` التي رأيناها في التجربة، والمختبر الذي يثبت من المسار يحصل على التحديث التالي تلقائياً. وفي Android تحديداً، الاختبار المغلق ليس خياراً للحسابات الشخصية الجديدة كما رأينا، فاجعل المسار المغلق جزءاً من خطتك من الأسبوع الأول.

وأثناء الاختبار، استخدم أدوات الاختبار الآلي على النسخة التي ستنشرها، لا على نسخة التصحيح، وقد شرحنا تشغيل سيناريوهات الواجهة في مقال [اختبار تطبيقات الجوال مع Appium وMaestro](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D8%B9-appium-%D9%88-maestro/). والسبب ما رأيناه في قسم R8: نسخة الإصدار مختلفة في الكود نفسه، وعطل الانعكاس لا يظهر إلا فيها.

## أسباب الرفض الشائعة Rejection Reasons وكيف تتجنبها

الجدول التالي يجمع أسباب الرفض التي يقع فيها تطبيق مثل تطبيقنا، مع المنصة والإرشاد أو المتطلب، والطريقة العملية لتجنبه قبل الإرسال:

| السبب                                | المنصة والمرجع                            | كيف تتجنبه                                                                           |
| ------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------ |
| المراجع لا يستطيع تسجيل الدخول       | Apple 2.1                                 | حساب تجريبي بكلمة مرور أو رمز ثابت في معلومات المراجعة، والسيرفر يعمل أثناء المراجعة |
| عطل أو شاشة فارغة عند المراجع        | Apple 2.1، Google قبل الإنتاج             | اختبر نسخة الإصدار نفسها على جهاز حقيقي وبأحدث نظام، لا نسخة التصحيح                 |
| نص مؤقت أو صور غير نهائية            | Apple 2.1 و2.3                            | راجع كل الشاشات والصور، ولقطات الشاشة من التطبيق وهو يعمل                            |
| رابط سياسة الخصوصية أو الدعم معطل    | Apple 5.1.1(i)، Google Data Safety        | رابط يعمل في المتجر وداخل التطبيق، وصفحة تشرح البيانات والحذف                        |
| لا يوجد حذف للحساب                   | Apple 5.1.1(v)، Google Account Deletion   | زر حذف داخل التطبيق، ورابط ويب لطلب الحذف في Google                                  |
| تسجيل دخول بطرف ثالث فقط             | Apple 4.8                                 | أضف خياراً مكافئاً يحفظ الخصوصية، أو تسجيلاً بالبريد أو الجوال                       |
| غلاف لموقع الويب فقط                 | Apple 4.2                                 | وظائف أصلية حقيقية: إشعارات، عمل دون اتصال، واجهة مصممة للجوال                       |
| تطبيق لكل فرع أو عميل                | Apple 4.3                                 | تطبيق واحد باختيار الفرع داخله                                                       |
| طريقة دفع خاطئة                      | Apple 3.1.1 و3.1.3(e)                     | السلع المادية بالبطاقة أو Apple Pay، والميزات الرقمية بالشراء داخل التطبيق           |
| نموذج البيانات لا يطابق سلوك التطبيق | Google Data Safety، Apple Privacy Details | اقرأ الملف المدمج وملفات Privacy Manifest للمكتبات، واملأ النموذج منها               |
| مستوى API مستهدف قديم                | Google target API                         | targetSdk 36 للتحديثات والتطبيقات الجديدة منذ 31 أغسطس 2026                          |
| مكتبة أصلية غير مصفوفة على 16 KB     | Google page sizes                         | zipalign -c -P 16 -v 4 على الملف النهائي، وحدث المكتبة القديمة                       |
| رفع حزمة برقم إصدار مستخدم           | Google versioning                         | رقم versionCode يزيد تلقائياً من نظام البناء                                         |
| حزمة موقعة بمفتاح خاطئ               | Google Play App Signing                   | تحقق من البصمة بالأمر keytool -list -v قبل الرفع                                     |

## أخطاء شائعة Common Mistakes

- ملف المفاتيح على جهاز مطور واحد دون نسخة احتياطية: إذا كنت لا تستخدم Play App Signing فهذا يعني أن التطبيق كله معلق بقرص واحد. احفظ الملف وكلمة المرور في مكانين منفصلين على الأقل، واحدهما خزنة أسرار.
- كتابة كلمة المرور في `build.gradle.kts` أو في ملف داخل المستودع: أي شخص يقرأ المستودع يستطيع توقيع تحديث باسمك إذا وصل إلى الملف.
- توزيع نسخ تجريبية موقعة بمفتاح التصحيح على عملاء: رأينا أن الانتقال منها إلى نسخة المتجر يحتاج حذف التطبيق وبياناته.
- استخدام بصمة مفتاح الرفع في `assetlinks.json` أو في إعدادات خدمات Google: الجوال يرى مفتاح التطبيق الذي تحتفظ به Google، فخذ البصمة من صفحة App Signing.
- تعديل الملف بعد التوقيع في خط البناء: رأينا `DOES NOT VERIFY` من إضافة ملف واحد.
- عدم حفظ ملف `mapping.txt` مع كل إصدار: تقارير الأعطال من المستخدمين ستأتي بأسماء مختصرة لا يمكنك قراءتها.
- تغيير مفتاح التوقيع مباشرة دون سلسلة نسب: الجهاز يرفض التحديث كما رأينا، والتدوير الصحيح يمر بالأمر `apksigner rotate` أو بخيار التدوير في Play Console.
- بدء الاختبار المغلق في الأسبوع الأخير: الحسابات الشخصية الجديدة تحتاج 14 يوماً على الأقل مع 12 مختبراً قبل طلب الإنتاج.
- افتراض أن الرفض نهاية الطريق: في 2024 قبلت Apple 295,109 طلباً بعد رفضها، فاقرأ رسالة الرفض بدقة، وأجب عن المراجع من App Store Connect إذا كان قد فهم شيئاً بشكل خاطئ.

## قائمة تحقق Checklist قبل الإرسال إلى المتجرين

هذه القائمة رتبناها بالترتيب الزمني الذي كان يجب أن يتبعه فريقنا، من الحساب إلى زر الإرسال:

- الحسابات: حساب Apple Developer Program وحساب Google Play، ويفضل حساب منظمة، وابدأ التحقق قبل الإطلاق بأسابيع لا بأيام.
- الاختبار المغلق في Google Play بدأ قبل 14 يوماً على الأقل مع 12 مختبراً إذا كان الحساب شخصياً.
- مفتاح الرفع بصيغة PKCS12 وصلاحية 25 سنة أو أكثر، مع نسختين احتياطيتين، وكلمة المرور في خزنة أسرار.
- Play App Signing مفعل، وبصمة مفتاح التطبيق مسجلة في كل خدمة تحتاجها.
- في iOS: شهادة Apple Distribution صالحة، والملف التعريفي يحمل كل القدرات التي يستخدمها التطبيق.
- `targetSdk` 36، والتطبيق مختبر بعد رفع الرقم، والبناء في iOS بإصدار Xcode 26 أو أحدث.
- R8 مفعل، وملف `mapping.txt` محفوظ ومرفوع إلى أداة تقارير الأعطال.
- `apksigner verify --print-certs` يعطي البصمة المتوقعة، و`zipalign -c -P 16 -v 4` ينجح على الملف النهائي.
- الصلاحيات في الملف المدمج مقروءة ومفهومة، ونموذج أمان البيانات وتفاصيل الخصوصية مملوءة منها، وكل مكتبة iOS تحمل ملف Privacy Manifest.
- سياسة الخصوصية ورابط الدعم يعملان، داخل التطبيق وفي المتجر.
- حذف الحساب داخل التطبيق، ورابط ويب لطلب الحذف.
- حساب تجريبي للمراجعين مع شرح أي إعداد خاص، والسيرفر يعمل طوال فترة المراجعة.
- لقطات الشاشة من التطبيق وهو يعمل، والاسم ثلاثون حرفاً أو أقل، ولا يوجد نص مؤقت.
- خطة إطلاق تدريجي بنسبة صغيرة، ومن يراقب الأعطال في الساعات الأولى، ومتى نوقف الإطلاق.

## الخلاصة Summary

وصلنا لنهاية الموضوع، وأهم ما فيه:

- التوقيع في Android يربط كل تحديث بمفتاح واحد، والجهاز رفض أمامنا التحديث بمفتاح مختلف بالرسالة `INSTALL_FAILED_UPDATE_INCOMPATIBLE`، ورفض الرجوع إلى رقم إصدار أقدم بالرسالة `INSTALL_FAILED_VERSION_DOWNGRADE`.
- Play App Signing يفصل مفتاح الرفع الذي يمكن إعادة تعيينه عن مفتاح التطبيق الذي تحتفظ به Google، وهو إلزامي للتطبيقات الجديدة منذ أغسطس 2021.
- R8 جعل تطبيق Compose الصغير 927,869 بايت بدلاً من 8,196,328، وثمنه ملف mapping تحفظه وكود انعكاس تختبره في نسخة الإصدار.
- التوقيع v2 وما بعده يحمي كل بايت، وإضافة ملف واحد بعد التوقيع أعطت `DOES NOT VERIFY`.
- تدوير المفتاح يحتاج سلسلة نسب يوقع فيها القديم على الجديد، والجهاز بنظام Android 15 استخدم المفتاح الجديد من كتلة v3.1، والأجهزة الأقدم من Android 13 تبقى على القديم.
- حزمة AAB بحجم 2.2 MB أصبحت لجوال عربي بمعالج arm64 ثلاثة ملفات بحجم تنزيل مقدر 781,753 بايت.
- Google Play في 2026: targetSdk 36 منذ 31 أغسطس، واختبار مغلق 14 يوماً مع 12 مختبراً للحسابات الشخصية الجديدة، وصفحات 16 KB، والتحقق من هوية المطور يبدأ في دول محددة ويتوسع في 2027.
- Apple رفضت في 2024 نحو ربع الطلبات، وقسم الأداء الذي يضم اكتمال التطبيق هو الأكبر، وأغلب أسبابه تمنع بقائمة تحقق قبل الإرسال.

ومقالات هذه المرحلة الأخرى، ومعها مقالات الجوال المنشورة:

[![](https://arabroot.io/content/images/2026/10/mobile-native-vs-cross-cover-cover.webp)دليلتطبيق الجوال: أصيل Native أم Flutter أم React Native؟بنينا نفس تطبيق الطلبات ثلاث مرات بلغة Kotlin وبإطاري Flutter وReact Native، وقسنا الحجم وزمن البناء والإقلاع والذاكرة لنعرف من أين يأتي كل فرق وكيف تختار.](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D8%A7%D8%B5%D9%8A%D9%84-native-%D8%A7%D9%85-flutter-%D8%A7%D9%85-react-native/)[![](https://arabroot.io/content/images/2026/10/mobile-app-lifecycle-cover-cover.webp)دليلدورة حياة تطبيق الجوال Lifecycle والمهام الخلفيةلماذا يجد المستخدم السلة فارغة بعد مكالمة، وما الذي يبقى فعلاً بعد تدوير الشاشة وموت العملية والسبات Doze، بتجربة حقيقية على Android وشرح ما يقابلها في iOS.](https://arabroot.io/articles/%D8%AF%D9%88%D8%B1%D8%A9-%D8%AD%D9%8A%D8%A7%D8%A9-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-lifecycle-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D9%85%D9%87%D8%A7%D9%85-%D8%A7%D9%84%D8%AE%D9%84%D9%81%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/mobile-offline-first-cover-cover.webp)دليلتطبيق يعمل دون إنترنت Offline-first: التخزين والمزامنةنبني سيرفراً وجوالين بقواعد SQLite ونرى بأرقام حقيقية كيف يضيع التسليم أو يسجل المبلغ مرتين أو يعود الطلب المحذوف، وكيف يمنع ذلك Outbox ومفتاح منع التكرار ودمج الحقول.](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D9%8A%D8%B9%D9%85%D9%84-%D8%AF%D9%88%D9%86-%D8%A7%D9%86%D8%AA%D8%B1%D9%86%D8%AA-offline-first-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D9%88%D8%A7%D9%84%D9%85%D8%B2%D8%A7%D9%85%D9%86%D8%A9/)[![](https://arabroot.io/content/images/2026/10/mobile-deep-links-push-cover-cover.webp)دليلالروابط العميقة Deep Links والإشعارات والشراء داخل التطبيقلماذا يفتح رابط الطلب المتصفح بدلاً من تطبيقك، وكيف يثبت نطاقك ملكيته للتطبيق في Android وiOS، وكيف يفتح الإشعار الشاشة الصحيحة، ومتى يلزمك الشراء داخل التطبيق.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B1%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D8%B9%D9%85%D9%8A%D9%82%D8%A9-deep-links-%D9%88%D8%A7%D9%84%D8%A7%D8%B4%D8%B9%D8%A7%D8%B1%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B4%D8%B1%D8%A7%D8%A1-%D8%AF%D8%A7%D8%AE%D9%84-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82/)[![](https://arabroot.io/content/images/2026/10/mobile-crash-basics-cover-cover.webp)دليلتقارير الأعطال Crash Reporting في تطبيقات الجوالكيف يصلك عطل حدث في جوال مستخدم لا تعرفه، وكيف تقرأ ال Stack Trace المشفر، ومتى توقف إصداراً جديداً قبل أن يصل إلى الجميع.](https://arabroot.io/articles/%D8%AA%D9%82%D8%A7%D8%B1%D9%8A%D8%B1-%D8%A7%D9%84%D8%A7%D8%B9%D8%B7%D8%A7%D9%84-crash-reporting-%D9%81%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84/)[![](https://arabroot.io/content/images/2026/10/mobile-crash-lab-cover-cover.webp)دليلتقارير الأعطال Crash Reports لتطبيق Android مع GlitchTipتطبيق Android يتعطل عمداً على المحاكي ويرسل أعطاله إلى GlitchTip على سيرفرك، وما الذي يعمل فعلاً عند رفع ملف ال Mapping وما الذي لا يعمل.](https://arabroot.io/articles/%D9%85%D8%AE%D8%AA%D8%A8%D8%B1-%D8%A7%D8%B9%D8%B7%D8%A7%D9%84-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-android-%D9%85%D8%B9-glitchtip/)[![](https://arabroot.io/content/images/2026/10/mobile-testing-cover-cover.webp)دليلاختبار تطبيقات الجوال مع Appium وMaestroكيف تختار أداة اختبار تطبيقات الجوال، وأين تشغل الاختبارات، ولماذا تفشل بعض الاختبارات عندما تصبح الواجهة عربية.](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D8%B9-appium-%D9%88-maestro/)[![](https://arabroot.io/content/images/2026/10/interview-mobile-cover-cover.webp)دليلأسئلة مقابلة مطور تطبيقات الجوال من المبتدئ إلى الخبيربنك أسئلة لمقابلة مطور تطبيقات الجوال على Android وiOS، لكل سؤال إجابة المبتدئ وإجابة الخبير، مع تمارين عملية شغلناها.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A6%D9%84%D8%A9-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%85%D8%B7%D9%88%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AE%D8%A8%D9%8A%D8%B1/)

وإذا كنت تستعد لمقابلة عمل، فراجع أسئلة [رفض Apple للتطبيق](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A6%D9%84%D8%A9-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%85%D8%B7%D9%88%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AE%D8%A8%D9%8A%D8%B1/#mo-review) و[النشر الآمن دون تراجع](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A6%D9%84%D8%A9-%D9%85%D9%82%D8%A7%D8%A8%D9%84%D8%A9-%D9%85%D8%B7%D9%88%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AE%D8%A8%D9%8A%D8%B1/#mo-staged) في مقال أسئلة المقابلة.

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ تجربة التوقيع والتغليف على تطبيق Kotlin وCompose بإصدار Android Gradle Plugin 9.4.1، مع keytool وjarsigner من JDK 21.0.5، وapksigner وzipalign وaapt2 من Build-Tools 37.0.0، وbundletool 1.18.3، على محاكي Android 15 (API 35). متطلبات المتجرين مقتبسة من صفحاتها الرسمية كما كانت في أكتوبر 2026، وإرشادات Apple بتحديث 16 أكتوبر 2026.