App Store & Play Store Development

كل عملية شراء في App Store تحتاج إلى appAccountToken، وإلا فلن يكون لدفاعك عن الاسترداد ما يسنده

عندما يطلب عميل استرداد أمواله، ترسل Apple إشعار CONSUMPTION_REQUEST الذي لا يذكر اسمه إطلاقًا. appAccountToken هو UUID الذي يربط عملية الشراء بمستخدمك. اضبطه لتتمكن من الرد ببيانات حقيقية؛ وتجاهله فتصبح تخمينًا محضًا.

5 min read
كل عملية شراء في App Store تحتاج إلى appAccountToken، وإلا فلن يكون لدفاعك عن الاسترداد ما يسنده

اثنتا عشرة ساعة هي كل ما تمنحه Apple لك لتبرير طلب استرداد بالأدلة. هذا الوقت المحدود عديم الفائدة إذا كانت المعاملة التي أمامك تخص عميلاً لا تستطيع تحديد هويته. appAccountToken هو UUID الذي يوفر هذه الهوية، ولا يمكن إرفاقه إلا أثناء حدوث عملية الشراء.

تخيّل وصول الإشعار. تريد Apple معرفة مقدار ما استهلكه هذا المشتري فعليًا من المنتج، ومنذ متى وهو عميل لديك، وما مجمل إنفاقه. أسئلة منطقية، وقاعدة بياناتك تحتفظ بكل إجابة. المشكلة أن الحمولة (payload) التي أمامك توفر originalTransactionId ومعرّف منتج، ولا يقابل أي منهما شيئًا في جدول المستخدمين لديك. أنت تمسك فاتورة بلا اسم عليها، والإجابات التي تحتاجها مصنّفة تحت أسماء.

لهذا التعارض حل، وللحل موعد نهائي. appAccountToken هو UUID يولّده نظامك، ويسلّمه إلى StoreKit أثناء إتمام الشراء، ثم تضعه Apple على المعاملة الناتجة. ومنذ تلك اللحظة، يصلك كل ما تخبرك به Apple عن تلك العملية وهو مُعرَّف الهوية مسبقًا. أما إذا فاتتك تلك اللحظة، فستبقى عملية البيع المعنية مجهولة الهوية للأبد. فيما يلي: ما هو هذا الحقل، وكيفية إرفاقه، ونقطة النهاية (endpoint) التي تُنقذ بأثر رجعي المشتريات التي لم يرَها تطبيقك قط، وحساب ما يحذفه فعليًا استرداد مجهول الهوية من أعمالك.

أبرز النقاط

  • أرفق UUID بعملية شراء في App Store، وستشير المعاملة الناتجة عنها مباشرة إلى مستخدم واحد محدد في سجلاتك. تحتفظ Apple بتلك القيمة على المعاملة وتعيدها إليك أينما ظهرت تلك المعاملة.

  • الصيغة هي الشيء الوحيد الذي تراقبه Apple، وهذه الصيغة هي UUID. أدخل معرّف قاعدة بيانات، أو عنوان بريد إلكتروني، أو سلسلة نصية مُلفّقة، وسيتجاهلها StoreKit دون أي اعتراض، تاركًا لك قيمة nil في موضع appAccountToken.

  • يعرض StoreKit 2 هذا الحقل كخيار شراء واحد، Product.PurchaseOption.appAccountToken(_:)، يُغذّى بـ UUID يولّده نظامك ويكون قد سُجّل مسبقًا مقابل ذلك الحساب.

  • أرفقه بعملية الشراء الأولى، وستنشر Apple القيمة نفسها عبر كل تجديد، وكل محاولة إعادة فوترة، وكل ترقية على طول سلسلة الاشتراك.

  • اعتبارًا من عام 2025، تمنح نقطة النهاية Set App Account Token خادمك وسيلة لوسم المشتريات التي لم يرها تدفق الشراء داخل التطبيق قط، بما في ذلك استخدام رموز العروض (offer codes) والمشتريات المروَّجة.

  • لا يصبح طلب CONSUMPTION_REQUEST من Apple قابلاً للرد إلا بفضل هذا الرمز المميز (token). فبدونه، لا يمكنك ربط طلب الاسترداد بالعميل نفسه الذي يُفترض أن تفصّل استهلاكه خلال نافذة الاثنتي عشرة ساعة.

  • طلب الاسترداد الذي يتعذّر تحديد هويته هو طلب يتعذّر الدفاع عنه. تُعاد الأموال عن مبيعات كنت تملك دليلاً عليها، وتذهب معها تكاليف الحوسبة واستدعاءات API والمدفوعات التي أُنفقت بالفعل في تسليمها.

ما الذي يفعله هذا الحقل فعليًا

تتعامل Apple مع هذه القيمة باعتبارها عديمة المعنى، وهذا بالضبط هو المقصود. أي UUID تسلّمه إلى StoreKit أثناء إتمام الشراء يُكتب على المعاملة ويُحفظ إلى أجل غير مسمى، دون أي محاولة لتفسيره أو التحقق مما يشير إليه أو التأكد من أنه يُحلَّل إلى أي شيء على الإطلاق. يصف تعريف Apple نفسه هذا الحقل بأنه "الـ UUID الذي يربط المعاملة بحساب المستخدم على خدمتك الخاصة". أما التفسير فهو شأنك أنت بالكامل. تكتفي Apple بحمل هذه القيمة وإعادتها إليك عند الطلب.

ما يجعل هذا الحقل مفيدًا هو أين تُعاد هذه القيمة إليك. إشعارات الخادم تحملها داخل المعاملة الموقّعة. واطلب من App Store Server API الحصول على Get Transaction Info فستجدها هناك أيضًا. وكل تجديد لاحق في سلسلة الاشتراك يُعيد إنتاجها، بشرط أن تكون عملية الشراء الأصلية قد حملتها. بعبارة أخرى، أرفق القيمة مرة واحدة، وستستمر في الظهور طوال الفترة التي يظل فيها ذلك العميل يدفع لك.

مرّر أي شيء غير UUID وستختفي القيمة دون أي تحذير

عادةً ما يكتشف المطورون هذه القاعدة عند مخالفتها. يتم تمرير شيء بدا منطقيًا، معرّف حساب داخلي، أو عنوان بريد إلكتروني، أو حقلين متصلين ببعضهما، ولا يبدي StoreKit أي اعتراض على الإطلاق. لا استثناء، ولا تحذير، ولا فشل في عملية الشراء. ببساطة تعود المعاملة وقيمة appAccountToken فيها nil، والرابط الذي افترضت وجوده لم يُنشأ قط. تحفل منتديات مطوري Apple بإمداد ثابت من هذا الالتباس بعينه، يُصاغ عادةً بعبارة "appAccountToken is missing in the transaction payload"، ويكاد التفسير يكون دائمًا قيمة فشلت في استيفاء متطلب الصيغة.

هذا المتطلب ضيق: يقبل StoreKit 2 معرّف UUID وفق معيار RFC 4122 ولا شيء غيره. لذا فإن النمط الآمن هو سكّ (توليد) الـ UUID على خادمك، وحفظه مقابل الحساب، والتأكد من أن أي نوع آخر من القيم لا يمكنه أبدًا الوصول إلى استدعاء الشراء.

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

إرفاق الرمز المميز أثناء إتمام الشراء

خيار شراء واحد هو التنفيذ بأكمله. ولّد الـ UUID من جانب الخادم عند التسجيل، أو عند وصول العميل لأول مرة إلى صفحة الدفع، واكتبه في سجل حسابه، ثم زوّد تلك القيمة نفسها في كل مرة يشتري فيها.

توقيع هذا الخيار هو Product.PurchaseOption.appAccountToken(_ token: UUID)، والاستدعاء نفسه يبدو كالتالي: try await product.purchase(options: [.appAccountToken(token)]). وأيًا كانت الطريقة التي تصلك بها المعاملة المكتملة، سواء تحققت منها بنفسك مقابل App Store Server API أو تلقيتها كإشعار خادم، فإن ذلك الـ UUID يرافقها في الداخل، مما يختزل تحديد هوية العميل إلى عملية بحث واحدة مفهرسة.

رمز واحد لكل عميل، يُعاد استخدامه إلى الأبد

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

نقطة النهاية التي تُنقذ المشتريات التي لم يلمسها تطبيقك قط

كان هناك مساران للشراء يُفشلان هذا الأمر تمامًا. استخدام رمز عرض (offer code) يحدث داخل App Store، وليس داخل تطبيقك. وكذلك شراء عملية شراء داخل التطبيق مروَّجة من صفحة إدراج في المتجر. لا يُنفّذ أي من المسارين كود الشراء الخاص بك، ما يعني عدم وجود لحظة يمكن فيها توفير رمز مميز، وبقيت تلك المعاملات مجهولة الهوية بشكل دائم.

انتهى هذا القيد في WWDC 2025 مع إطلاق نقطة النهاية Set App Account Token. يصدر خادمك طلب PUT /inApps/v1/transactions/{originalTransactionId}/appAccountToken إلى App Store Server API، حاملاً الـ UUID في نص الطلب (body)، وتكتبه Apple على المعاملة. تمتد التغطية لتشمل كل أنواع المنتجات، بما في ذلك استخدام رموز العروض والمشتريات المروَّجة بالتأكيد، والقيمة المُرسلة بهذه الطريقة تحل محل أي رمز كانت المعاملة تحمله بالفعل. وبذلك أصبح تحديد الهوية أمرًا يمكنك القيام به لاحقًا، من جانب الخادم بالكامل، على مشتريات لم يكن تطبيقك متورطًا فيها إطلاقًا.

يعتمد مكان إرفاق الرمز المميز على كيفية حدوث عملية الشراء:

  • بالنسبة لعملية شراء عادية داخل التطبيق، اضبطه في خيار الشراء الخاص بـ StoreKit في لحظة الشراء، عبر Product.PurchaseOption.appAccountToken(UUID).

  • بالنسبة لـاستخدام رمز عرض (offer code)، اضبطه عبر نقطة النهاية Set App Account Token من خادمك، لأنه لا يوجد شيء داخل التطبيق يمكن الاتصال به ويتعيّن عليك إرفاقه لاحقًا.

  • بالنسبة لعملية شراء داخل التطبيق مروَّجة من App Store، اضبطه بالطريقة نفسها عبر نقطة النهاية Set App Account Token، لأن المعاملة تحدث خارج نطاق تطبيقك.

  • بالنسبة لـتجديد الاشتراك، لا تفعل شيئًا. يُورَّث الرمز المميز تلقائيًا من عملية الشراء الأصلية.

ما الذي يفعله الرابط المكسور بإيراداتك

الأمر هنا لا يتعلق بترتيب السجلات. الأمر هو أن Apple تطرح على المطورين سؤالاً واحدًا بالتحديد أثناء طلب الاسترداد، يُسلَّم في صورة CONSUMPTION_REQUEST، والإجابة عنه تتطلب معرفة أي من عملائك هو المقصود.

تُطلق طلبات استرداد الأموال الخاصة بالمنتجات الاستهلاكية والاشتراكات غير المتجددة هذا الإشعار، وSend Consumption Information هو طريقة ردّك، خلال اثنتي عشرة ساعة. تتوقع Apple تفاصيل دقيقة عن فرد واحد: نسبة المنتج المستهلكة، مدة العضوية، الإنفاق الإجمالي، حالة التسليم. يظهر الرمز المميز كحقل ضمن الطلب نفسه، لكن دوره الأهم هو دور بنيوي، لأنه الشيء الوحيد القادر على تحويل ذلك الإشعار إلى سطر في قاعدة بياناتك الخاصة. انزع الرمز المميز وسيصبح الطلب بلا إجابة ممكنة، بغض النظر عن مدى دقة تتبعك للاستخدام.

ثمن الرد بلا شيء

يتبقى خياران عندما يصل طلب استرداد مجهول الهوية، ولا أحدهما جيد. قدّم ردّ استهلاك فارغًا، وستفسّره Apple على أنه استخدام ضئيل، مما يرجّح كفة القرار نحو الموافقة، حتى بالنسبة للمستخدمين كثيفي الاستخدام. أو اختلق تقديرًا بدلاً من ذلك، فتكون بذلك تقدّم خيالاً إلى جهة مالكة المنصة. وأيًا كان المسار الذي تسلكه، تُسترد المبيعات التي كان بإمكانك إثباتها على أي حال، بعد أن دفعت بالفعل لتلبيتها.

تكلفة التلبية هي الجزء المؤلم، لأنها لا تمت بصلة لسعر البيع. استدعاءات API الخاصة بالنموذج المستهلكة أثناء التوليد، والصور المُنتجة، وفيديو تمت معالجته وتصديره، ودفعة لصانع محتوى صُرفت لاحقًا في السلسلة: كل هذه الأموال غادرت حساباتك عند التسليم، واسترداد العميل لا يفعل شيئًا لاستعادتها. والآن وسّع نطاق المشكلة. عميل واحد مجهول الهوية يقدّم طلبات متكررة. المسترِدّون المعتادون يستهدفون تحديدًا الشركات التي لا تستطيع تحديد هويتهم. عند تلك النقطة يتوقف الرمز المميز المتجاهَل عن كونه إغفالاً صغيرًا ويبدأ بالعمل كصنبور مفتوح.

سؤال Apple يتعلق بالاستهلاك الحقيقي لعميل واحد، والإجابة عنه تتطلب معرفة هوية ذلك العميل. بدون الرمز المميز، تخسر مبيعات كان بإمكانك إثباتها، إلى جانب تكاليف الحوسبة وإنفاق API الذي أُنفق بالفعل في تسليمها.

يُنفّذ Android الخطة نفسها تحت مسمى مختلف

تؤدي setObfuscatedAccountId هذه المهمة في Google Play، إذ تربط معرّف حساب بعملية شراء بحيث تحصل مراجعة رد المبالغ (chargeback) عبر orders.reviewrefund على شخص فعلي لفحصه. تختلف التطبيقات (implementations) اختلافًا كبيرًا، لكن المبدأ الأساسي ينتقل دون تعديل: إما أن تُربط الهوية أثناء المعاملة، أو يُخسر النزاع افتراضيًا لاحقًا. نسخة Apple من هذا المبدأ هي appAccountToken، وهي تصرّ على أن تكون القيمة UUID.

ثلاث ممارسات تبقي هوية كل عملية شراء معروفة

  • وَلِّد UUID واحدًا لكل حساب واحفظه. رمز مميز واحد ثابت لكل عميل، يُنشأ عند التسجيل أو أول عملية دفع، ويُكتب في سجله، ويُعاد استخدامه في كل ما يشتريه.

  • تحقق من القيمة قبل تسليمها. تأكد فعليًا من أنها UUID داخل كود الشراء الخاص بك، حتى لا يتحول معرّف مشوّه بصمت أبدًا إلى رمز مميز قيمته nil على المعاملة.

  • املأ بأثر رجعي أي شيء تم شراؤه خارج التطبيق. عندما يصل إشعار خادم لاستخدام رمز عرض أو عملية شراء مروَّجة لا تحمل رمزًا مميزًا، استخدم نقطة النهاية Set App Account Token وأرفق الرمز الصحيح.

اعتمد الممارسات الثلاث جميعها، وسيتوقف تحديد الهوية عن كونه شيئًا تحاول القيام به بعد وقوع الحدث. سيصبح خاصية تمتلكها كل معاملة بالفعل عندما تسلّمها Apple إليك، بما في ذلك طلبات الاسترداد بالتأكيد.

بُني Refund Sensor مباشرة على هذا الافتراض. نستخرج appAccountToken من كل معاملة وإشعار خادم، ونربطه ببيانات الاستهلاك المسجَّلة بالفعل لذلك الحساب، ونقدّم ردًا على طلب Apple قبل انتهاء نافذة الاثنتي عشرة ساعة بوقت كافٍ، مستخدمين أرقامًا تصف ذلك المشتري وصفًا حقيقيًا. طلب الاسترداد الذي يمكنك تسميته هو طلب يمكنك الجدال بشأنه، وهو النوع الوحيد الذي يستحق الجدال بشأنه أصلاً.

أين تم توثيق هذه القواعد

الأسئلة الشائعة

هو UUID يملكه نظامك، ويكتفي StoreKit بتسجيله فقط. تزوّده أنت أثناء إتمام الشراء، وتكتبه Apple على المعاملة الناتجة، ومنذ ذلك الحين يظهر مجددًا أينما ظهرت تلك المعاملة: في معلومات المعاملة الخاصة بها، وفي أي إشعار خادم يتعلق بها، وفي كل تجديد لاحق ضمن السلسلة نفسها. الأثر العملي هو أن أي شيء تخبرك به Apple لاحقًا، بما في ذلك طلب استرداد، يمكن ربطه بالسجل الصحيح في قاعدة بياناتك الخاصة.

غالبًا ما تكون المشكلة في الصيغة. يعتمد StoreKit 2 معيار RFC 4122 كمعيار مقبول، وأي قيمة تفشل في استيفاء هذا الاختبار تُستبعد دون أي خطأ، تاركة nil بينما تكتمل عملية الشراء وكأن شيئًا لم يحدث خطأ. معرّفات السجلات الداخلية، وعناوين البريد الإلكتروني، والسلاسل النصية المُلفّقة، كلها تفشل بهذه الطريقة. الاحتمال الآخر هو أن تطبيقك لم يعالج عملية الشراء على الإطلاق، وغالبًا ما يكون استخدام رمز عرض (offer code) هو السبب، إذ لم يُنفَّذ أي كود لديك لتوفير رمز مميز.

منذ عام 2025، نعم. أدخلت Apple نقطة النهاية Set App Account Token إلى App Store Server API لهذه الحالة بالتحديد. طلب PUT إلى /inApps/v1/transactions/{originalTransactionId}/appAccountToken، مع تزويد UUID في نص الطلب، يكتب رمزًا على معاملة موجودة بالفعل، أو يستبدل رمزًا كان موجودًا هناك سابقًا. تشمل هذه الميزة كل أنواع المنتجات، وقد صُمِّمت خصيصًا للمشتريات التي لم يتعامل معها تطبيقك قط، وتحديدًا رموز العروض والمشتريات المروَّجة.

نعم، دون أي استثناء. ما يشير إليه الـ UUID من جانبك يظل شأنك الخاص تمامًا ولا تتفحصه Apple أبدًا، لكن القيمة نفسها يجب أن تستوفي صيغة UUID. إذا فشلت في ذلك، يرفض StoreKit الاحتفاظ بها، تاركًا nil على المعاملة.

تُطلق طلبات الاسترداد إشعار CONSUMPTION_REQUEST، وتتوقع Apple ردًا خلال اثنتي عشرة ساعة يصف كيفية استخدام ذلك المشتري تحديدًا للمنتج. إنتاج مثل هذا الرد يعني تحويل المعاملة الواردة في الإشعار إلى سجل عميل يمكنك فعليًا الاستعلام عنه، وهذا بالضبط ما تقوم به وظيفة الرمز المميز. كما أنه يظهر كحقل ضمن الطلب نفسه. بدونه، يكون ردك إما فارغًا أو ملفّقًا، والرد الفارغ عادة ما يؤدي إلى منح الاسترداد.

#appaccounttoken#storekit#refunds#in-app purchases#ios
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers