اكتشاف الاسترداد في StoreKit 2 يتلخص في خاصية واحدة على الـ transaction، وهي revocationDate
عندما يعيد Apple المال لأحد عملائك، فإن الاسترداد يكون موجوداً بالفعل داخل تطبيقك على revocationDate الخاص بالـ transaction، قبل أن تعمل مهمة الخادم لديك. هنا نوضح أين يظهر اكتشاف الاسترداد في StoreKit 2 على الجهاز، وما الذي يخبرك به revocationDate و revocationReason، ولماذا العميل للسرعة والخادم للحقيقة.

أهم الخلاصات
- في StoreKit 2 يحمل الـ purchase المُسترد قيمة revocationDate غير nil على الـ Transaction الخاص به، لذا يمكن لتطبيقك اكتشاف الاسترداد بنفسه، دون استدعاء خادمك.
- يتم تعيين revocationDate عندما يعيد App Store المال عن transaction أو عندما يفقده عميل عبر Family Sharing، لذا فإن التاريخ غير nil لا يعني دائماً استرداداً.
- يخبرك revocationReason بالسبب: developerIssue يعني أن العميل ذكر مشكلة في تطبيقك، و other يغطي كل سبب استرداد آخر متبقٍ.
- يستبعد Transaction.currentEntitlements بالفعل المشتريات المُستردة والملغاة، لذا فإن أنظف بوابة من جانب العميل للوصول هي ببساطة ما إذا كان الـ product لا يزال يظهر هناك.
- لا يسلّم Transaction.updates استرداداً حدث بينما كان تطبيقك مغلقاً إلا إذا بدأت الاستماع عند التشغيل، لذا فإن غياب Task يعني استرداداً فائتاً.
- الاكتشاف من جانب العميل يعمل فقط بينما التطبيق مفتوح، ولهذا يبقى REFUND في App Store Server Notifications V2 هو الإشارة الموثوقة التي توقفك عن الدفع لخدمة مستخدم مُسترد.
- يمكن عكس الاسترداد، وعندما يحدث ذلك، تُزال حقول الـ revocation من الـ transaction ويُتوقع منك استعادة الوصول الذي قطعته.
تعرف معظم التطبيقات عن استرداد Apple من خادمها، عبر App Store Server Notification، ولا تلاحظ أبداً أن الاسترداد نفسه موجود بالفعل داخل التطبيق. إنه على الـ transaction، في خاصية اسمها revocationDate، وقراءتها تتيح لتطبيقك قطع وصول عميل مُسترد في المرة التالية التي يفتحه فيها، بدلاً من انتظار مهمة خلفية. اكتشاف الاسترداد في StoreKit 2 هو إشارة من جانب العميل تتخطاها معظم الفرق. هنا نوضح بالضبط أين يظهر الاسترداد على الجهاز، وما الذي يخبرك به، وما لا يخبرك به، ولماذا يجب أن يكون بجانب إشعارات خادمك وليس بديلاً عنها.
أين يظهر الاسترداد داخل StoreKit 2
يسلّمك StoreKit 2 المعاملات كقيم موقّعة، والاسترداد لا يحذف الـ transaction. إنه يضع عليه علامة. خاصيتان على الـ Transaction تحملان العلامة، وكلاهما يبقى nil طوال حياة الـ purchase السليم. عندما تصبح إحداهما غير nil، يكون App Store قد استعاد الـ purchase.
revocationDate هو الحقل الذي ينقلب
revocationDate هو Date اختياري. وصف Apple نفسه دقيق: إنه التاريخ الذي أعاد فيه App Store المال عن الـ transaction أو ألغاه من Family Sharing. بالنسبة لـ purchase لا يزال جيداً، يكون nil. في اللحظة التي تتم فيها معالجة استرداد، يحمل الطابع الزمني لذلك الاسترداد. هذا الفحص الوحيد، هل revocationDate غير nil، هو كامل اكتشاف الاسترداد من جانب العميل. كل ما عدا ذلك دقائق مكدسة فوقه.
revocationReason يخبرك لماذا سحبه Apple
revocationReason يقع بجانب التاريخ ويشرح السبب. يمنحه StoreKit قيمتين مهمتين للاسترداد. developerIssue يعني أن العميل أخبر Apple أن الاسترداد كان بسبب مشكلة فعلية أو متصوَّرة في تطبيقك. other يغطي كل سبب متبقٍ. قيمة ثالثة، upgradedToBundle، ليست استرداداً على الإطلاق؛ إنها تشير إلى transaction ألغاه App Store لأن العميل انتقل إلى subscription bundle. اقرأ السبب قبل أن تتصرف، لأن developerIssue هو ما يستحق العدّ: مجموعة منها هي منتجك نفسه يخبرك أين انكسر.
| Property | Type | ماذا تعني قيمة غير nil |
|---|---|---|
revocationDate | Date? | أعاد App Store المال عن هذا الـ transaction، أو ألغاه عبر Family Sharing، في هذا التاريخ |
revocationReason هو developerIssue | reason | ذكر العميل مشكلة فعلية أو متصوَّرة في تطبيقك |
revocationReason هو other | reason | حدث الاسترداد لسبب آخر لا يفصّله Apple |
revocationReason هو upgradedToBundle | reason | ليس استرداداً؛ أُلغي الـ transaction لأن العميل انتقل إلى subscription bundle |
currentEntitlements يُسقط بالفعل الـ purchase المُسترد
لست مضطراً دائماً لقراءة حقول الـ revocation بنفسك. Transaction.currentEntitlements هو تسلسل المشتريات التي لا يزال العميل مستحقاً لها الآن، ويبنيه Apple ليستبعد تلك التي لا ينبغي أن تكرمها. الـ product الذي أعاد App Store المال عنه أو ألغاه لا يظهر فيه. ولا الاشتراكات المنتهية، ولا الـ consumables، التي تختفي لحظة انتهائها.
هذا يجعل currentEntitlements أنظف بوابة للوصول. اسأله عمّا يملكه العميل، امنح ذلك بالضبط، والاسترداد يزيل الـ entitlement نيابة عنك دون فحص revocationDate واحد. حقول الـ revocation لأجل عندما تريد التفاصيل، التاريخ والسبب، لتسجيل الحدث أو التفاعل معه. قائمة الـ entitlement لأجل السؤال البسيط عمّا إذا كان يجب إبقاء الأضواء مضاءة.
اكتشاف الاسترداد في StoreKit 2 عملياً، من التشغيل وأثناء عمل التطبيق
هناك لحظتان يمكن لتطبيقك أن يلتقط فيهما استرداداً على الجهاز، وتحتاجان إلى كود مختلف. الأولى بينما التطبيق مفتوح ويحدث استرداد مباشرة أو على جهاز آخر. الأخرى عند التشغيل، للحاق بكل ما تغيّر بينما كنت مغلقاً. أخفق في الثانية وسيكون في اكتشاف الاسترداد في StoreKit 2 لديك ثغرة بالضبط حيث تسقط معظم عمليات الاسترداد، لأن العملاء نادراً ما يكون تطبيقك مفتوحاً لديهم عندما يطلبون استرداداً.
ابدأ الاستماع عند التشغيل وإلا فاتتك عمليات الاسترداد التي حدثت وأنت مغلق
Transaction.updates هو التسلسل غير المتزامن الذي يصدر transaction كلما أنشأ النظام أو حدّث واحداً خارج تطبيقك أو على جهاز آخر، بما في ذلك الاسترداد. تعليمات Apple صريحة: ابدأ Task يتكرر عليه بمجرد تشغيل تطبيقك، وإلا فقد تفوتك المعاملات التي يسلّمها مرة واحدة فقط عند البدء. استرداد وصل أثناء الليل يصل عبر updates في المرة التالية التي يُفتح فيها التطبيق، لكن فقط إذا كان هناك listener يعمل بالفعل لاستقباله. لا listener، لا event، ويبقى الاسترداد غير مرئي حتى يوفّقه شيء آخر.
الـ purchase على الجهاز نفسه لا يأتي عبر updates
فخّ واحد يوقع من يختبرون الاسترداد يدوياً. الـ purchase العادي الذي يتم على الجهاز نفسه لا يصل عبر updates؛ يعيده StoreKit مباشرة من نتيجة استدعاء الـ purchase. updates للتغييرات خارج النطاق: عمليات الاسترداد، موافقات Ask to Buy، استبدالات offer-code، والمشتريات التي تتم في مكان آخر. لذا ابنِ معالجة الاسترداد لديك حول updates و currentEntitlements، وليس حول تدفق الـ purchase، لأن الاسترداد لن يعود أبداً عبر المسار الذي سلكته عملية البيع.

ما لا يستطيع الاكتشاف من جانب العميل فعله لك
قراءة عمليات الاسترداد على الجهاز سريعة ومجانية، لكن لها سقف، والتظاهر بعكس ذلك هو كيف تتسرب الإيرادات. الجهاز يعرف فقط ما أخبره به StoreKit، و StoreKit يتحدث فقط بينما تطبيقك يعمل. العميل الذي يحصل على استرداد ولا يفتح تطبيقك مجدداً أبداً هو عميل لا يراه فحصك من جانب العميل قط.
الـ revocationDate ليس دائماً استرداداً
الحقل نفسه ينقلب لأجل Family Sharing. عندما يفقد عميل الوصول إلى purchase مشترك، لأن المنظّم أزاله أو انتهت المشاركة، يحصل ذلك الـ transaction على revocationDate أيضاً. لذا فإن تاريخاً غير nil يعني أن العميل لم يعد يملك هذا الـ purchase، وهو بالضبط ما تحتاجه للتحكم في الوصول، لكنه لا يعني دائماً أن المال عاد. إذا كنت تعدّ عمليات الاسترداد للإيرادات، افصل إلغاءات Family Sharing عن الحقيقية قبل أن تثق بالرقم.
يمكن عكس الاسترداد
الاسترداد ليس دائماً نهائياً. يمكن لـ Apple عكسه، وعندما يفعل، تُزال حقول الـ revocation من الـ transaction ويصبح الـ purchase صالحاً مجدداً. إذا قطعت الوصول عند الاسترداد، يُتوقع منك استعادته عند العكس. على الجهاز يظهر ذلك كـ event آخر من updates بـ transaction نظيف؛ على خادمك هو notification متميز اسمه REFUND_REVERSED. عالج الاسترداد فقط وستترك عميلاً يدفع عالقاً بلا وصول وبإيصال يعمل.
ما يكلّفه فعلاً الإلغاء المتأخر
نادراً ما يكون الاسترداد مجرد خروج سعر البيع من حسابك. بحلول وقت تسويته تكون عادة قد أنفقت بالفعل مالاً حقيقياً لخدمة ذلك الـ purchase، وذلك الإنفاق لا يعود. الصور المُنشأة تكلّف دقائق GPU. إجابات الدردشة تكلّف استدعاءات model API فُوترت عليك لكل token. عمليات الرفع تكلّف تخزيناً لا تزال تدفع للاحتفاظ به. إذا موّل الـ purchase دفعة لمُنشئ محتوى، فذلك المال خرج بالفعل. لا شيء من ذلك ينعكس مع الاسترداد.
الاكتشاف من جانب العميل يقلّص النافذة على الجزء الوحيد الذي لا تزال تتحكم فيه، وهو الإنفاق المستقبلي. كلما عرفت أبكر أن purchase قد استُرد، توقفت أبكر عن خدمته. لكن الجهاز يخبرك فقط بينما التطبيق مفتوح، لذا فإن مستخدماً مُسترداً لا يعود أبداً يحتفظ بأي وصول من جانب الخادم منحته، مكلفاً إياك بهدوء في كل مرة تتصرف فيها مهمة خلفية أو جهاز مُتزامن نيابة عنه. العميل يجعل الإلغاء سريعاً. لا يجعله مضموناً.
استخدم العميل للسرعة والخادم للحقيقة
التصميم النظيف يستخدم الإشارتين لما تجيده كل منهما. على الجهاز، يمنحك Transaction.updates و currentEntitlements رد فعل فورياً محلياً في اللحظة التي يفتح فيها عميل مُسترد التطبيق، جيد للواجهة ولإنهاء تغييرات الـ entitlement دون رحلة ذهاب وإياب. على الخادم، ترسل App Store Server Notifications V2 رسالة REFUND تصل سواء فُتح التطبيق مجدداً أم لا، وهي الإشارة الوحيدة التي توقف خلفيتك بشكل موثوق عن الإنفاق على حساب مُسترد.
| Signal | أين يعيش | يعمل عندما | ثِق به لـ |
|---|---|---|---|
revocationDate على transaction | Device، StoreKit 2 | يقرأ تطبيقك الـ transaction | يخبرك أن purchase محدداً استُرد أو أُلغي |
Transaction.updates | Device، StoreKit 2 | يصل استرداد بينما التطبيق يعمل، أو عند التشغيل إن استمعت | التفاعل فوراً لعميل حاضر |
currentEntitlements | Device، StoreKit 2 | تفحص ما يملكه العميل الآن | بوابة الوصول دون تتبّع عمليات الاسترداد بنفسك |
REFUND notification | خادمك، App Store Server Notifications V2 | يعالج Apple الاسترداد، سواء فُتح التطبيق أم لا | إيقاف الإنفاق من جانب الخادم على عميل لا يعود أبداً |
وصّل إشارات الجهاز للعميل الذي يمسك الهاتف، وإشعار الخادم للذي لا يمسكه. الاسترداد يظهر في كلا المكانين عن قصد. قراءة واحدة منهما فقط هي كيف يستمر حساب مُسترد في تكلفتك بعد أن تكون عملية البيع قد ذهبت بالفعل.
الأسئلة الشائعة
- كيف أكتشف استرداداً في StoreKit 2؟
- افحص revocationDate الخاص بالـ transaction. يكون nil لـ purchase صالح ويحمل تاريخاً بمجرد أن يعيد App Store المال عن الـ transaction، لذا فإن revocationDate غير nil هو الإشارة إلى أن purchase قد استُرد أو أُلغي.
- ما الفرق بين revocationDate و revocationReason؟
- revocationDate هو متى استعاد App Store الـ purchase، و revocationReason هو لماذا. السبب هو developerIssue عندما يذكر العميل مشكلة في تطبيقك و other لأي شيء آخر.
- هل يظهر الـ purchase المُسترد في currentEntitlements؟
- لا. يستبعد Transaction.currentEntitlements المشتريات التي أعاد App Store المال عنها أو ألغاها، لذا فإن product مُسترداً يسقط من entitlements العميل من تلقاء نفسه، مما يجعله بوابة آمنة للوصول.
- هل سيخبر StoreKit تطبيقي عن استرداد حدث بينما كان التطبيق مغلقاً؟
- فقط إذا استمعت من التشغيل. يسلّم Transaction.updates تلك التغييرات مرة واحدة عند البدء، لذا يجب أن تبدأ Task يتكرر عليه بمجرد تشغيل تطبيقك وإلا فُقد الاسترداد حتى يوفّقه شيء آخر.
- هل الاكتشاف من جانب العميل كافٍ بمفرده؟
- لا. يعرف الجهاز عن استرداد فقط بينما تطبيقك يعمل، لذا فإن عميلاً لا يعيد فتح التطبيق أبداً يكون غير مرئي له. REFUND في App Store Server Notifications V2 هو الإشارة التي تصلك بغض النظر عن ذلك.
- هل revocationDate يعني دائماً أن العميل حصل على استرداد؟
- لا. يُعيَّن revocationDate أيضاً عندما يفقد عميل purchase عبر Family Sharing، لذا فإن تاريخاً غير nil يعني أنه لم يعد يملك الـ purchase لكن ليس دائماً أن المال أُعيد.
المصادر وقراءات إضافية
- Apple Developer: Transaction.revocationDate (StoreKit)
- Apple Developer: Transaction.RevocationReason (StoreKit)
- Apple Developer: Transaction.currentEntitlements (StoreKit)
- Apple Developer: Transaction.updates (StoreKit)
- Apple Developer: App Store Server Notifications V2 notificationType (REFUND, REFUND_REVERSED)
- Apple Developer Tech Talk: Support customers with StoreKit 2 and App Store Server API
RefundHalt
الطيار الآلي للاستردادات في App Store وGoogle Play
تابع القراءة
كل نافذة استرداد في App Store وGoogle Play هي عدّ تنازلي، وإليك كم ساعة تمنحك كل واحدة منها
كل استرداد في App Store وGoogle Play يبدأ ساعة تعمل، ومعظمها يعمل من دونك. أقصر نافذة استرداد لدى Apple هي 12 ساعة، ونافذة رد المبالغ المدفوعة لدى Google هي 24 ساعة، واعتبارًا من 3 أغسطس 2026 فإن نافذة رد مبالغ فائتة تصبح فاتورة، وليست مجرد عملية بيع خاسرة. إليك كل مهلة تمسّ حسابك.
يتيح إشعار الشراء المُلغى لخادم Google Play الخاص بك إلغاء الوصول في اللحظة التي يصل فيها الاسترداد
يمكن لـ Google Play أن يدفع إلى خادمك إشعار شراء مُلغى في اللحظة التي يُسترد فيها الشراء أو يُرَدّ عبر السداد العكسي أو يُلغى. يحمل الإشعار purchaseToken و orderId و productType و refundType، ويعني شيئًا واحدًا، إلغاء الوصول. إليك كيفية قراءته وربطه.