StoreKit 2: ما الذي تغيّر في التعامل مع الاشتراكات
الإيصال لم يعد ملفًا يُرسل إلى خادم ليُفكّ، بل كائنًا موقّعًا يُقرأ محليًا. ما الذي يلغيه ذلك من الشيفرة، ومتى يبقى الخادم ضروريًا رغم كل شيء.

الإجابة المختصرة
StoreKit 2 يجعل الإيصال كائنًا موقّعًا يتحقق منه التطبيق محليًا بدل ملف يُرسل إلى خادم، ويعطيك حالة الاشتراك باستعلام واحد عبر الاستحقاقات. النتيجة أن التطبيق الذي يفتح ميزات داخله فقط لم يعد يحتاج بنية خلفية، ويبقى الخادم ضروريًا لإشعارات الإلغاء وللحسابات الممتدة خارج الجهاز.
في هذه الصفحة
كتبت كود الاشتراكات مرة قبل سنوات، وتعرف الشكل: قوائم انتظار، ومعاملات معلّقة، وتحقق من الإيصال على خادم، ومنطق يعيد المحاولة. StoreKit 2 هو الإصدار الثاني من واجهة المشتريات في منظومة آبل، وأهم ما فيه أنه يلغي أغلب ذلك الكود بدل أن يحسّنه.
الفرق ليس في عدد الدوال. الفرق أن الإيصال لم يعد ملفًا مبهمًا يجب إرساله إلى خادم ليُفكّ، بل صار كائنًا موقّعًا يمكن للتطبيق قراءته والتحقق منه محليًا.
ما الذي تغيّر فعلًا؟
ثلاثة تغييرات تحمل معظم الفائدة، وكلها تقلّل الشيفرة بدل أن تضيف إليها.
- المعاملات صارت غير متزامنة بلا قوائم انتظار.
await product.purchase()يعيد النتيجة مباشرة، بدل مراقب عام يتلقى الأحداث من مكان آخر. - الإيصال صار قابلًا للتحقق محليًا. كل معاملة تصل موقّعة بصيغة JWS، والنظام يتحقق من التوقيع نيابة عنك.
- حالة الاشتراك صارت سؤالًا مباشرًا.
Transaction.currentEntitlementsيعطيك ما يملكه المستخدم الآن، دون إعادة بناء التاريخ من معاملات قديمة.
النقطة الثانية هي الأهم. في الإصدار الأول كان التحقق الجاد يتطلب خادمًا يتحدث إلى آبل، وهو ما جعل تطبيقًا بسيطًا يحتاج بنية خلفية كاملة من أجل زر شراء واحد.
| المهمة | StoreKit الأول | StoreKit 2 |
|---|---|---|
| قراءة الإيصال | ملف مشفّر يُرسل إلى خادم | كائن موقّع يُقرأ محليًا |
| معرفة ما يملكه المستخدم | إعادة بناء من سجل المعاملات | استعلام واحد |
| استعادة المشتريات | استدعاء صريح ومنطق دمج | تُقرأ من الاستحقاقات مباشرة |
| اختبار محلي | Sandbox فقط | ملف تكوين داخل Xcode |
| الحد الأدنى للنظام | إصدارات قديمة | iOS 15 فما فوق |
الحاجة إلى خادم لم تختفِ. ما اختفى هو أن تكون شرطًا لأبسط الحالات.
هل ما زلت بحاجة إلى خادم؟
سؤال يستحق إجابة صريحة لأنه يحدد أسبوعًا من العمل.
لا تحتاجه إن كان الاشتراك يفتح ميزات داخل التطبيق نفسه فقط. التحقق المحلي كافٍ، والتوقيع يمنع التزوير، والاستحقاقات تعطيك الحالة الحالية.
تحتاجه في ثلاث حالات، ولا رابع لها عمليًا:
- حساب المستخدم يمتد خارج الجهاز — نسخة ويب، أو مزامنة بين مستخدمين.
- تريد إشعارات الخادم لتعرف بالإلغاء أو فشل التجديد دون أن يفتح المستخدم التطبيق.
- تحتاج سجلًا تدقيقيًا لأسباب محاسبية أو تعاقدية.
وهناك حالة رابعة يخلط فيها كثيرون: الرغبة في «منع القرصنة». الخادم لا يحلّها، والتوقيع المحلي في StoreKit 2 يعطي ضمانًا مماثلًا لما يعطيه التحقق البعيد. من يبني خادمًا لهذا السبب وحده يبني بنية لا تشتري له شيئًا.
الحالة الثانية هي التي تفاجئ الناس. من دون إشعارات الخادم، لا تعرف أن اشتراكًا أُلغي إلا حين يفتح المستخدم التطبيق مرة أخرى، وقد لا يفعل. لتطبيق مجاني بميزات مدفوعة هذا مقبول. لتطبيق يعتمد على دورة تجديد فليس كذلك.
كيف يبدو الحد الأدنى العامل؟
ثلاث قطع لا رابع لها في أبسط تطبيق يبيع اشتراكًا.
قراءة المنتجات. تطلب المعرّفات من المتجر وتعرض displayPrice كما وصلت. لا تحسب ضريبة ولا تحوّل عملة؛ المتجر فعل ذلك.
الشراء. استدعاء واحد ينتظر النتيجة. الحالات الثلاث التي يجب التعامل معها هي النجاح، وإلغاء المستخدم، والانتظار — والأخيرة تعني موافقة أبوية معلّقة، وهي التي تُنسى.
قراءة الاستحقاقات عند كل إقلاع ومع كل تغيّر. هذا ما يحدد ما يراه المستخدم، لا نتيجة الشراء التي حدثت مرة.
القطعة الثالثة هي التي تجعل الاستعادة تعمل دون زر «استعادة المشتريات» أصلًا: إن كنت تقرأ ما يملكه المستخدم الآن، فالمشتريات القديمة تظهر وحدها على أي جهاز يسجّل الدخول بالمعرّف نفسه.
StoreKit 2 والمشتريات غير المتجددة
الحديث عن الاشتراكات يخفي أن الواجهة نفسها تخدم ثلاثة أنواع مختلفة، ولكل منها سلوك مختلف في الاستحقاقات.
الاشتراك المتجدد يظهر في الاستحقاقات ما دام ساريًا، ويختفي عند انتهائه. هذا هو السلوك الذي يفترضه أغلب الكود.
الشراء غير القابل للاستهلاك — فتح مدى الحياة مثلًا — يبقى في الاستحقاقات إلى الأبد. لا تحتاج تخزينه، ولا يجب أن تنتهي صلاحيته في كودك.
الشراء الاستهلاكي لا يظهر في الاستحقاقات إطلاقًا، لأنه استُهلك. مسؤولية تتبّعه عليك أنت، وهذا هو الاستثناء الوحيد الذي يبرّر التخزين المحلي.
الخلط بين الثاني والثالث سبب شائع لشكوى «دفعت واختفى». من يعامل شراءً غير قابل للاستهلاك كأنه استهلاكي يخزّنه محليًا، ثم يفقده المستخدم عند تغيير الجهاز.
أين يخطئ الناس؟
أربعة أخطاء تتكرر، وثلاثة منها ورثناها من عادات الإصدار الأول.
تخزين حالة الاشتراك في `UserDefaults`. تكتب isPro = true بعد الشراء ثم تقرأها لاحقًا. المشكلة أنها لا تعرف بالإلغاء ولا بانتهاء المدة ولا بالاسترداد، فيبقى المستخدم «مشتركًا» بعد أن توقف. اقرأ الاستحقاقات؛ هي المصدر الوحيد.
إهمال مهمة الاستماع للمعاملات. المعاملة قد تصل خارج تدفق الشراء تمامًا — شراء من جهاز آخر، أو مشاركة عائلية، أو معاملة كانت معلّقة عند إغلاق التطبيق. من لا يشغّل مستمعًا عند الإقلاع يفقد هذه الحالات بصمت.
نسيان `finish()`. المعاملة التي لا تُنهى تعود عند كل إقلاع إلى الأبد. الأثر الظاهر: مستخدمون يشتكون من تكرار تأكيد الشراء.
افتراض أن الشراء يعني الاستحقاق فورًا. بين نجاح الشراء وظهوره في الاستحقاقات لحظة، ومن يقرأ الحالة مباشرة بعد الشراء قد يجدها فارغة فيعرض شاشة الدفع مرة أخرى. اقرأ من نتيجة المعاملة نفسها في تلك اللحظة، ومن الاستحقاقات فيما بعد.
عرض سعر مكتوب في الشيفرة. الأسعار تختلف بالعملة والضريبة والمنطقة، والقيمة الوحيدة الصحيحة هي displayPrice القادمة من المتجر. السعر المكتوب يدويًا يصبح خاطئًا لأول مستخدم خارج بلدك.
الاختبار صار أسهل، وهذا يغيّر طريقة العمل
في الإصدار الأول كان اختبار الاشتراكات يعني حسابات Sandbox وانتظارًا حقيقيًا بين التجديدات. الآن يمكن تعريف المنتجات في ملف تكوين داخل Xcode وتشغيلها محليًا، مع تسريع الزمن.
الفارق العملي أن الحالات النادرة صارت قابلة للاختبار فعلًا:
- فشل التجديد لأن البطاقة رُفضت.
- الاسترداد بعد أن استخدم المستخدم الميزة.
- الترقية والتخفيض بين مستويين.
- فترة السماح بعد فشل الدفع وقبل انتهاء الاستحقاق.
هذه الأربع هي التي تنتج شكاوى الدعم لاحقًا، وكانت قبلًا تُختبر بالأمل. وقد كتبنا عن قرارات الاشتراك التي تُتخذ مرة وتعيش سنوات في بناء المنتجات، وعن قياس ما قبل الإطلاق في تجارب المشاريع.
الهجرة من الإصدار الأول
إن كان لديك كود يعمل، فالسؤال ليس «هل أهاجر؟» بل «ما الذي أهاجره أولًا؟».
الترتيب الذي يقلّل المخاطرة يبدأ من القراءة لا من الكتابة:
- استبدل قراءة الحالة أولًا. اقرأ الاستحقاقات بدل منطقك القديم، واترك مسار الشراء كما هو. هذا وحده يزيل أكثر الأخطاء شيوعًا.
- أضف مستمع المعاملات بجوار المراقب القديم، ثم أزل القديم بعد إصدار مستقر.
- انقل الشراء أخيرًا، لأنه المسار الذي يراه المستخدم ويدفع فيه.
- احتفظ بالتحقق على الخادم إن كان موجودًا حتى تتأكد أن المحلي يعطي النتيجة نفسها.
الخطوة الرابعة تستحق أسبوعًا من التشغيل المتوازي. أن تقارن ما يقوله الخادم بما يقوله الجهاز على مستخدمين حقيقيين هو الطريقة الوحيدة لاكتشاف الحالات التي لم تخطر لك، مثل الاسترداد الجزئي أو تغيير المنطقة.
ولا تهاجر ونت تطلق ميزة جديدة في الإصدار نفسه. إن ظهرت شكوى، تريد أن تعرف أيّ التغييرين سببها.
متى لا يكون StoreKit 2 الخيار الصحيح؟
حالتان، وكلاهما عن التوافق لا عن الجودة.
إن كنت تدعم أنظمة أقدم من iOS 15. الواجهة الجديدة غير متاحة هناك، فتحتاج المسارين معًا. كتابة اثنين أسوأ من كتابة واحد قديم، والقرار الصحيح هو رفع الحد الأدنى إن كان جمهورك يسمح.
إن كان لديك خادم اشتراكات يعمل بالفعل ويخدم منصات أخرى. إعادة كتابة التحقق محليًا لا تعطيك شيئًا لم يكن لديك، وتضيف مسارًا ثانيًا يجب أن يتفق مع الأول.
فيما عدا ذلك، الاتجاه واضح: شيفرة أقل، وحالات خطأ أقل، ونقطة فشل واحدة أقل بين المستخدم والميزة التي دفع مقابلها. وثائق آبل لـ StoreKit تغطي التفاصيل، وتستحق قراءة قسم الاستحقاقات كاملًا قبل كتابة أول سطر. أما الجانب المعماري لدعم منصتين فتناولناه في تطوير التطبيقات.
الخلاصة
StoreKit 2 يستبدل بنية خلفية كاملة باستعلام محلي في الحالة الشائعة. اقرأ الاستحقاقات بدل تخزين حالة، وشغّل مستمع المعاملات عند الإقلاع، وأنهِ كل معاملة، ولا تكتب سعرًا بيدك.
وإن كنت تحتاج معرفة الإلغاء قبل أن يفتح المستخدم التطبيق، فأنت تحتاج خادمًا — وهذا سؤال منتج قبل أن يكون سؤالًا تقنيًا.
أسئلة شائعة
- هل يمكن الاستغناء عن الخادم تمامًا مع StoreKit 2؟
- نعم إن كان الاشتراك يفتح ميزات داخل التطبيق فقط، لأن التحقق المحلي من التوقيع كافٍ. تحتاج خادمًا إن أردت معرفة الإلغاء قبل أن يفتح المستخدم التطبيق، أو إن امتد الحساب إلى الويب.
- لماذا لا أخزّن حالة الاشتراك في UserDefaults؟
- لأنها لا تعرف بالإلغاء ولا بانتهاء المدة ولا بالاسترداد، فيبقى المستخدم مشتركًا بعد أن توقف. الاستحقاقات هي المصدر الوحيد الذي يعكس الحالة الحالية.
- ماذا يحدث إن لم أستدعِ finish على المعاملة؟
- تعود المعاملة عند كل إقلاع للتطبيق إلى الأبد، ويظهر ذلك للمستخدم كتكرار في تأكيد الشراء. الإنهاء هو ما يخبر النظام أنك سلّمت المحتوى.
- هل أحتاج حسابات Sandbox للاختبار؟
- لم تعد ضرورية للحالة الشائعة. ملف تكوين المتجر داخل Xcode يشغّل المنتجات محليًا ويسمح بتسريع الزمن، فتصبح حالات مثل فشل التجديد والاسترداد قابلة للاختبار فعلًا.
المصادر
- StoreKit — الوثائق الرسمية — Apple Developer
- In-app purchase — App Store Connect — Apple Developer
- App Store Server Notifications V2 — Apple Developer
نشرته
MrBebo
محتوى تقني عربي يقدمه بهاء طه حول البرمجة، الذكاء الاصطناعي، Web2 وWeb3، تطوير التطبيقات، بناء المنتجات وأدوات المطورين.
عن المدوّنة
