إضافة عمليات الشراء داخل التطبيق إلى تطبيق Flutter

1. مقدمة

تاريخ التعديل الأخير: 11/07/2023

لإضافة عمليات شراء داخل التطبيق إلى تطبيق Flutter، يجب إعداد التطبيق ومتاجر Play بشكل صحيح والتأكّد من عملية الشراء ومنح الأذونات اللازمة، مثل مزايا الاشتراك.

في هذا الدرس التطبيقي حول الترميز، ستضيف ثلاثة أنواع من عمليات الشراء داخل التطبيق (المتوفّرة لك) وستتحقّق من عمليات الشراء هذه باستخدام خلفية Dart مع Firebase. يحتوي التطبيق المقدَّم، Dash Clicker، على لعبة تستخدم رمز Dash كعملة. ستضيف خيارات الشراء التالية:

  1. خيار شراء قابل للتكرار لـ 2000 شرط في آن واحد.
  2. عملية شراء ترقية لمرة واحدة لتحويل لوحة القيادة من النمط القديم إلى لوحة مفاتيح عصرية.
  3. اشتراك يضاعف النقرات التي يتم إنشاؤها تلقائيًا.

يمنح خيار الشراء الأول المستخدم ميزة مباشرة لـ 2000 شرط. تتوفر هذه البطاقات للمستخدمين مباشرةً ويمكن شراؤها عدة مرات. ويسمى هذا الاستهلاك نوعًا من الاستهلاك حيث يتم استهلاكه بشكل مباشر ويمكن استهلاكه عدة مرات.

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

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

تعمل خدمة الخلفية (المتوفرة أيضًا لك) كتطبيق Dart، وتتأكّد من إتمام عمليات الشراء، وتخزّنها باستخدام Firestore. تُستخدَم Firestore لتسهيل هذه العملية، ولكن في تطبيق الإنتاج، يمكنك استخدام أي نوع من خدمات الخلفية.

300123416ebc8dc1.png 7145d0fffe6ea741.png 646317a79be08214.png

ما الذي ستقوم ببنائه

  • سيتم توسيع نطاق التطبيق ليشمل عمليات الشراء والاشتراكات الاستهلاكية.
  • ويمكنك أيضًا توسيع نطاق تطبيق واجهة Dart الخلفية للتحقّق من العناصر التي تم شراؤها وتخزينها.

ما الذي ستتعلّمه

  • كيفية ضبط App Store و"متجر Play" مع المنتجات القابلة للشراء.
  • كيفية التواصل مع المتاجر لتأكيد عمليات الشراء وتخزينها في Firestore
  • كيفية إدارة عمليات الشراء في تطبيقك

المتطلبات

  • الإصدار 4.1 من "استوديو Android" أو إصدار أحدث
  • Xcode 12 أو إصدار أحدث (لتطوير iOS)
  • Flutter SDK

2. إعداد بيئة التطوير

لبدء هذا الدرس التطبيقي حول الترميز، نزِّل الرمز وغيِّر معرّف الحزمة لنظام التشغيل iOS واسم الحزمة لنظام التشغيل Android.

تنزيل الرمز

لاستنساخ مستودع جيت هب من سطر الأوامر، استخدم الأمر التالي:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

أو إذا كانت لديك أداة cli في GitHub مثبّتة، استخدِم الأمر التالي:

gh repo clone flutter/codelabs flutter-codelabs

يتم نسخ نموذج الرمز البرمجي إلى دليل flutter-codelabs يحتوي على الرمز البرمجي لمجموعة من الدروس التطبيقية حول الترميز. يتوفّر رمز هذا الدرس التطبيقي حول الترميز باللغة flutter-codelabs/in_app_purchases.

تحتوي بنية الدليل ضمن flutter-codelabs/in_app_purchases على سلسلة من اللقطات للمكان الذي يجب أن تكون فيه في نهاية كل خطوة مُعنوَنة. تجد رمز البدء في الخطوة 0، لذا فإن تحديد موقع الملفات المطابقة أمر سهل مثل:

cd flutter-codelabs/in_app_purchases/step_00

إذا كنت تريد التخطي للأمام أو مشاهدة ما يجب أن يبدو عليه بعد خطوة ما، فابحث في الدليل الذي يحمل اسم الخطوة التي تريدها. يمكنك العثور على رمز الخطوة الأخيرة ضمن المجلد complete.

إعداد مشروع التفعيل

افتح مشروع إجراء التفعيل من "step_00" في بيئة تطوير البرامج (IDE) المفضّلة لديك. استخدمنا "استوديو Android" لأخذ لقطات الشاشة، لكنّ Visual Studio Code هو أيضًا خيار رائع. باستخدام أي من المحرِّرين، تأكّد من تثبيت أحدث مكوّنَين إضافيَين لتطبيقَي Dart وFlutter

يجب التواصل مع التطبيقات التي ستنشئها مع App Store و"متجر Play" لمعرفة المنتجات المتوفّرة وسعرها. يتم تحديد كل تطبيق من خلال معرّف فريد. يُعرف ذلك باسم معرّف الحزمة بالنسبة إلى App Store على أجهزة iOS، وهذا هو معرّف التطبيق في "متجر Play" على أجهزة Android. ويتم إنشاء هذه المعرّفات عادةً باستخدام تدوين اسم مجال عكسي. على سبيل المثال، عند إنشاء تطبيق للشراء داخل التطبيق من أجل flutter.dev، سنستخدم dev.flutter.inapppurchase. فكر في معرف لتطبيقك، ستقوم الآن بتعيينه في إعدادات المشروع.

أولاً، يجب إعداد معرّف الحزمة لنظام التشغيل iOS.

بعد فتح المشروع في "استوديو Android"، انقر بزر الماوس الأيمن على مجلد iOS، ثم انقر على Flutter، وافتح الوحدة في تطبيق Xcode.

942772eb9a73bfaa.png

في بنية مجلد Xcode، يظهر مشروع Runner في أعلى الصفحة، بينما تظهر الأهداف Flutter وRunner وProducts أسفل مشروع Runner. انقر مرّتين على Runner لتعديل إعدادات المشروع، ثم انقر على توقيع الإمكانات. أدخل معرّف الحزمة الذي اخترته للتو ضمن حقل الفريق لتحديد فريقك.

812f919d965c649a.jpeg

ويمكنك الآن إغلاق Xcode والرجوع إلى "استوديو Android" لإكمال عملية الإعداد على جهاز Android. لإجراء ذلك، افتح ملف build.gradle ضمن android/app, وغيِّر applicationId (في السطر 37 في لقطة الشاشة أدناه) إلى معرّف التطبيق، وهو نفسه معرّف حزمة iOS. يُرجى العلم أنّه من غير الضروري أن تكون معرّفات متاجر iOS وAndroid متطابقة، إلا أنّ إبقائها متطابقة أقل عرضة للخطأ، وبالتالي سنستخدم أيضًا معرّفات متطابقة في هذا الدرس التطبيقي حول الترميز.

5c4733ac560ae8c2.png

3- تثبيت المكوّن الإضافي

في هذا الجزء من الدرس التطبيقي حول الترميز، ستثبِّت المكوّن الإضافي in_app_purchase.

إضافة تبعية في pubspec

أضف in_app_purchase إلى pubspecs عن طريق إضافة in_app_purchase إلى التبعيات في pubspec:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

dependencies:
  ..
  cloud_firestore: ^4.0.3
  firebase_auth: ^4.2.2
  firebase_core: ^2.5.0
  google_sign_in: ^6.0.1
  http: ^0.13.4
  in_app_purchase: ^3.0.1
  intl: ^0.18.0
  provider: ^6.0.2
  ..

انقر على pub get لتنزيل الحزمة أو شغِّل flutter pub get في سطر الأوامر.

4. إعداد App Store

لإعداد عمليات الشراء داخل التطبيق واختبارها على نظام التشغيل iOS، عليك إنشاء تطبيق جديد في App Store وإنشاء منتجات قابلة للشراء هناك. ليس عليك نشر أي تطبيق أو إرساله إلى Apple لمراجعته. لتنفيذ هذا الإجراء، يجب أن يكون لديك حساب مطوِّر. إذا لم يكن لديك حساب، سجِّل في برنامج مطوّري برامج Apple.

لاستخدام عمليات الشراء داخل التطبيق، يجب أيضًا أن يكون لديك اتفاقية نشطة للتطبيقات المدفوعة في App Store Connect. انتقِل إلى https://appstoreconnect.apple.com/، وانقر على الاتفاقيات والضرائب والمعاملات المصرفية.

6e373780e5e24a6f.png

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

74c73197472c9aec.png

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

4a100bbb8cafdbbf.jpeg

تسجيل رقم تعريف التطبيق

أنشِئ معرّفًا جديدًا في بوابة مطوّري برامج Apple.

55d7e592d9a3fc7b.png

اختيار أرقام تعريف التطبيقات

13f125598b72ca77.png

اختيار تطبيق

41ac4c13404e2526.png

قدِّم بعض الأوصاف وحدِّد معرِّف الحزمة لمطابقة معرِّف الحزمة مع القيمة نفسها التي تم ضبطها سابقًا في XCode.

9d2c940ad80deeef.png

لمزيد من الإرشادات عن كيفية إنشاء رقم تعريف تطبيق جديد، يُرجى الاطّلاع على مساعدة حساب المطوِّر .

إنشاء تطبيق جديد

أنشِئ تطبيقًا جديدًا في App Store Connect باستخدام معرّف الحزمة الفريد.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

للحصول على مزيد من الإرشادات حول كيفية إنشاء تطبيق جديد وإدارة الاتفاقيات، يُرجى الاطّلاع على مركز مساعدة App Store Connect.

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

3ca2b26d4e391a4c.jpeg

يمكنك الآن إعداد مستخدم وضع الحماية على جهاز iPhone من خلال الانتقال إلى الإعدادات > App Store > Sandbox-account

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

ضبط عمليات الشراء داخل التطبيق

ستقوم الآن بتهيئة العناصر الثلاثة القابلة للشراء:

  • dash_consumable_2k: عملية شراء استهلاكية يمكن شراؤها عدة مرات، وتمنح المستخدم 2000 شرط (العملة داخل التطبيق) لكل عملية شراء.
  • dash_upgrade_3d: "ترقية" غير قابلة للاستهلاك عملية الشراء التي لا يمكن شراؤها سوى مرة واحدة، وتمنح المستخدم لوحة تحكم مختلفة من الناحية الجمالية للنقر عليها.
  • dash_subscription_doubler: اشتراك يمنح المستخدم ضعف عدد الشرطات لكل نقرة خلال مدة الاشتراك.

d156b2f5bac43ca8.png

انتقِل إلى عمليات الشراء داخل التطبيق >. إدارة:

إنشاء عمليات الشراء داخل التطبيق باستخدام المعرّفات المحدّدة:

  1. يمكنك إعداد "dash_consumable_2k" باعتباره مستهلكًا.

استخدِم dash_consumable_2k كمعرّف المنتج. لا يتم استخدام الاسم المرجعي إلا في App Store Connect، ما عليك سوى ضبطه على dash consumable 2k وإضافة ترجمات عملية الشراء. يمكنك استدعاء عملية الشراء "Spring is in the air" باستخدام الوصف "2000 dashes fly out".

ec1701834fd8527.png

  1. يمكنك إعداد dash_upgrade_3d على أنّه غير قابل للاستهلاك.

استخدِم dash_upgrade_3d كمعرّف المنتج. اضبط الاسم المرجعي على dash upgrade 3d وأضِف ترجماتك إلى عملية الشراء. يمكنك استدعاء عملية الشراء "3D Dash" باستخدام الوصف "Brings your dash back to the future".

6765d4b711764c30.png

  1. إعداد dash_subscription_doubler كاشتراك يتم تجديده تلقائيًا.

يختلف مسار الاشتراكات قليلاً. عليك أولاً ضبط الاسم المرجعي ومعرّف المنتج:

6d29e08dae26a0c4.png

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

5bd0da17a85ac076.png

بعد ذلك، أدخِل مدة الاشتراك وعمليات الأقلمة. يمكنك تسمية هذا الاشتراك باسم Jet Engine مع الوصف Doubles your clicks. انقر على حفظ (Save).

bd1b1d82eeee4cb3.png

بعد النقر على الزر حفظ، أضِف سعر الاشتراك. اختَر أي سعر تريده.

d0bf39680ef0aa2e.png

من المفترض أن تظهر الآن عمليات الشراء الثلاث في قائمة المشتريات:

99d5c4b446e8fecf.png

5- إعداد "متجر Play"

وكما هي الحال في App Store، يجب أيضًا أن يكون لديك حساب مطوِّر على "متجر Play". إذا لم يكُن لديك حساب بعد، سجِّل حسابًا.

إنشاء تطبيق جديد

إنشاء تطبيق جديد في Google Play Console:

  1. افتح Play Console.
  2. اختَر جميع التطبيقات > إنشاء تطبيق
  3. اختَر لغة تلقائية وأضف عنوانًا لتطبيقك. اكتب اسم التطبيق كما تريد أن يظهر على Google Play. يمكنك تغييره لاحقًا.
  4. يجب تحديد أن تطبيقك لعبة. يمكنك تغييرها لاحقًا.
  5. حدِّد ما إذا كان تطبيقك مجانيًا أو مدفوعًا.
  6. أضِف عنوان بريد إلكتروني يمكن لمستخدمي "متجر Play" استخدامه للتواصل معك بشأن هذا التطبيق.
  7. إكمال إرشادات المحتوى وبيانات قوانين التصدير الأمريكية.
  8. اختَر إنشاء تطبيق.

بعد إنشاء تطبيقك، انتقِل إلى لوحة البيانات وأكمِل جميع المهام في قسم إعداد تطبيقك. في ما يلي بعض المعلومات عن تطبيقك، مثل التقييمات حسب الفئة العمرية ولقطات الشاشة. 13845badcf9bc1db.png

توقيع التطبيق

لتتمكّن من اختبار عمليات الشراء داخل التطبيق، يجب تحميل إصدار واحد على الأقل إلى Google Play.

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

إنشاء ملف تخزين مفاتيح

إذا كان لديك ملف تخزين مفاتيح حالي، انتقِل إلى الخطوة التالية. إذا لم يكن الأمر كذلك، فأنشئ واحدًا من خلال تشغيل ما يلي في سطر الأوامر.

على نظام التشغيل Mac/Linux، استخدم الأمر التالي:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

على نظام التشغيل Windows، استخدم الأمر التالي:

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

يخزِّن هذا الأمر ملف key.jks في الدليل الرئيسي. إذا أردت تخزين الملف في مكان آخر، غيِّر الوسيطة التي تضبطها إلى مَعلمة -keystore. الحفاظ على

keystore

file Private؛ يُرجى عدم التحقّق من ذلك في مراقبة المصادر العامة.

الإشارة إلى ملف تخزين المفاتيح من التطبيق

أنشئ ملفًا باسم "<your app dir>/android/key.properties" يحتوي على مرجع إلى ملف تخزين المفاتيح:

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>

ضبط إعدادات Gradle لتسجيل الدخول

يمكنك ضبط عملية تسجيل الدخول لتطبيقك من خلال تعديل ملف <your app dir>/android/app/build.gradle.

أضِف معلومات ملف تخزين المفاتيح من ملف مواقعك قبل حظر android:

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         // omitted
   }

حمِّل ملف key.properties إلى الكائن keystoreProperties.

أضِف الرمز التالي قبل مجموعة buildTypes:

   buildTypes {
       release {
           // TODO: Add your own signing config for the release build.
           // Signing with the debug keys for now,
           // so `flutter run --release` works.
           signingConfig signingConfigs.debug
       }
   }

اضبط الجزء signingConfigs في ملف build.gradle الخاص بالوحدة باستخدام معلومات إعداد التوقيع:

   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

سيتم الآن توقيع إصدارات إصدارات تطبيقك تلقائيًا.

لمزيد من المعلومات حول توقيع تطبيقك، يمكنك الاطّلاع على توقيع تطبيقك على developer.android.com.

تحميل الإصدار الأول

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

flutter build appbundle

ينشئ هذا الأمر إصدار إصدار تلقائيًا ويمكن العثور على المخرجات في <your app dir>/build/app/outputs/bundle/release/

من لوحة البيانات في Google Play Console، انتقِل إلى الإصدار > الاختبار > الاختبار المغلق وإنشاء إصدار جديد للاختبار المغلق

في هذا الدرس التطبيقي، عليك الالتزام بتوقيع التطبيق من خلال Google، لذا اضغط على متابعة ضمن ميزة "توقيع التطبيق" من Play للموافقة.

ba98446d9c5c40e0.png

بعد ذلك، يجب تحميل حِزمة تطبيق "app-release.aab" التي تم إنشاؤها بواسطة أمر الإصدار.

انقر على حفظ ثم على مراجعة إصدار التطبيق.

أخيرًا، انقر على بدء طرح الإصدار للاختبار الداخلي لتفعيل إصدار الاختبار الداخلي.

إعداد المستخدمين الاختباريين

لكي تتمكّن من اختبار عمليات الشراء داخل التطبيق، يجب إضافة حسابات Google للمختبِرين إلى وحدة تحكُّم Google Play في مكانين:

  1. إلى مسار الاختبار المحدد (الاختبار الداخلي)
  2. بصفتك مختبِرًا للترخيص

أولاً، ابدأ بإضافة المختبِر إلى مسار الاختبار الداخلي. الرجوع إلى الإصدار > الاختبار > الاختبار الداخلي وانقر على علامة التبويب المختبِرون.

a0d0394e85128f84.png

أنشئ قائمة عناوين بريد إلكتروني جديدة بالنقر على إنشاء قائمة عناوين بريد إلكتروني. أدخِل اسمًا للقائمة، ثم أضِف عناوين البريد الإلكتروني لحسابات Google التي تحتاج إلى إذن لاختبار عمليات الشراء داخل التطبيق.

بعد ذلك، ضَع علامة في مربّع الاختيار للقائمة، وانقر على حفظ التغييرات.

بعد ذلك، أضِف مختبِري الترخيص:

  1. ارجع إلى عرض جميع التطبيقات في Google Play Console.
  2. انتقِل إلى الإعدادات > اختبار الترخيص:
  3. يُرجى إضافة عناوين البريد الإلكتروني نفسها للمختبِرين المطلوب منهم السماح لهم باختبار عمليات الشراء داخل التطبيق.
  4. اضبط ردّ الترخيص على RESPOND_NORMALLY.
  5. انقر على حفظ التغييرات.

a1a0f9d3e55ea8da.png

ضبط عمليات الشراء داخل التطبيق

ستقوم الآن بتكوين العناصر التي يمكن شراؤها داخل التطبيق.

وكما هو الحال في App Store، يجب تحديد ثلاث عمليات شراء مختلفة:

  • dash_consumable_2k: عملية شراء استهلاكية يمكن شراؤها عدة مرات، وتمنح المستخدم 2000 شرط (العملة داخل التطبيق) لكل عملية شراء.
  • dash_upgrade_3d: "ترقية" غير قابلة للاستهلاك عملية الشراء التي لا يمكن شراؤها سوى مرة واحدة، مما يمنح المستخدم لوحة تحكم مختلفة من الناحية الجمالية للنقر عليها.
  • dash_subscription_doubler: اشتراك يمنح المستخدم ضعف عدد الشرطات لكل نقرة خلال مدة الاشتراك.

أضف أولاً السلع الاستهلاكية وغير الاستهلاكية.

  1. انتقِل إلى Google Play Console واختَر تطبيقك.
  2. انتقِل إلى تحقيق الربح > المنتجات > المنتجات داخل التطبيق:
  3. انقر على إنشاء منتجc8d66e32f57dee21.png.
  4. أدخِل جميع المعلومات المطلوبة لمنتجك. تأكَّد من أنّ معرِّف المنتج يتطابق تمامًا مع المعرّف الذي تنوي استخدامه.
  5. انقر على حفظ.
  6. انقر على تفعيل.
  7. كرِّر العملية المتعلقة بـ "الترقية" غير القابلة للاستهلاك. عملية الشراء.

بعد ذلك، أضِف الاشتراك:

  1. انتقِل إلى Google Play Console واختَر تطبيقك.
  2. انتقِل إلى تحقيق الربح > المنتجات > الاشتراكات:
  3. انقر على إنشاء اشتراك32a6a9eefdb71dd0.png.
  4. أدخِل جميع المعلومات المطلوبة لاشتراكك. تأكَّد من أنّ معرِّف المنتج يتطابق تمامًا مع المعرّف الذي تنوي استخدامه.
  5. انقر على حفظ.

من المفترض أن يتم الآن إعداد عمليات الشراء في Play Console.

6- إعداد Firebase

في هذا الدرس التطبيقي حول الترميز، ستستخدم خدمة الخلفية للتحقّق من هوية المستخدمين وتتبُّعهم. عمليات الشراء.

هناك العديد من المزايا لاستخدام خدمة الخلفية:

  • يمكنك إثبات صحة المعاملات بأمان.
  • يمكنك التفاعل مع أحداث الفوترة من متاجر التطبيقات.
  • يمكنك تتبع عمليات الشراء في قاعدة بيانات.
  • ولن يتمكن المستخدمون من خداع التطبيق لدفعه إلى تقديم ميزات مدفوعة من خلال إرجاع ساعة النظام.

على الرغم من توفُّر عدة طرق لإعداد خدمة الخلفية، يمكنك إجراء ذلك باستخدام وظائف السحابة الإلكترونية وFirestore باستخدام منصّة Firebase الخاصة بـ Google.

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

ويتم أيضًا تضمين مكونات Firebase الإضافية مع تطبيق إجراء التفعيل.

وما عليك سوى إنشاء مشروعك الخاص على Firebase، وإعداد كلٍّ من التطبيق والخلفية لبرنامج Firebase، وأخيرًا نشر الخلفية.

إنشاء مشروع على Firebase

انتقِل إلى وحدة تحكُّم Firebase، وأنشئ مشروع Firebase جديد. في هذا المثال، عليك باستدعاء مشروع Dash Clicker.

في تطبيق الخلفية، يمكنك ربط عمليات الشراء بمستخدم معيّن، وبالتالي تحتاج إلى المصادقة. ولإجراء ذلك، يمكنك الاستفادة من وحدة مصادقة Firebase مع تسجيل الدخول بحساب Google.

  1. من لوحة بيانات Firebase، انتقِل إلى صفحة المصادقة وفعِّلها إذا لزم الأمر.
  2. انتقِل إلى علامة التبويب طريقة تسجيل الدخول وفعِّل موفِّر خدمة تسجيل الدخول باستخدام Google.

7babb48832fbef29.png

لأنّك ستستخدم أيضًا قاعدة بيانات Firestore في Firebase، فعِّل هذا الخيار أيضًا.

e20553e0de5ac331.png

اضبط قواعد Cloud Firestore على النحو التالي:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /purchases/{purchaseId} {
      allow read: if request.auth != null && request.auth.uid == resource.data.userId
    }
  }
}

إعداد Firebase لبرنامج Flutter

الطريقة التي ننصح بها لتثبيت Firebase على تطبيق Flutter هي استخدام FlutterFire CLI. اتّبِع التعليمات كما هو موضّح في صفحة الإعداد.

عند إعداد flutterfire، اختَر المشروع الذي أنشأته للتوّ في الخطوة السابقة.

$ flutterfire configure

i Found 5 Firebase projects.                                                                                                  
? Select a Firebase project to configure your Flutter application with ›                                                      
❯ in-app-purchases-1234 (in-app-purchases-1234)                                                                         
  other-flutter-codelab-1 (other-flutter-codelab-1)                                                                           
  other-flutter-codelab-2 (other-flutter-codelab-2)                                                                      
  other-flutter-codelab-3 (other-flutter-codelab-3)                                                                           
  other-flutter-codelab-4 (other-flutter-codelab-4)                                                                                                                                                               
  <create a new project>  

بعد ذلك، يمكنك تفعيل iOS وAndroid من خلال اختيار النظامين الأساسيين.

? Which platforms should your configuration support (use arrow keys & space to select)? ›                                     
✔ android                                                                                                                     
✔ ios                                                                                                                         
  macos                                                                                                                       
  web                                                                                                                          

عندما يُطلب منك ذلك بشأن تجاوز firebase_options.dart، اختَر "نعم".

? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes                                                                                                                         

إعداد Firebase لنظام التشغيل Android: خطوات إضافية

من لوحة بيانات Firebase، انتقل إلى نظرة عامة على المشروع واختر الإعدادات ثم انقر على علامة التبويب عام.

انتقِل للأسفل وصولاً إلى تطبيقاتك، واختَر تطبيق dashclicker (android).

b22d46a759c0c834.png

للسماح بتسجيل الدخول إلى حساب Google في وضع تصحيح الأخطاء، يجب تقديم الملف المرجعي لتجزئة SHA-1 لشهادة تصحيح الأخطاء.

الحصول على تجزئة شهادة توقيع تصحيح الأخطاء

في جذر مشروع تطبيق Flutter، غيِّر الدليل إلى مجلد "android/" ثم أنشِئ تقرير توقيع.

cd android
./gradlew :app:signingReport

ستظهر لك قائمة كبيرة من مفاتيح التوقيع. لأنّك تبحث عن تجزئة شهادة تصحيح الأخطاء، ابحث عن الشهادة التي تم ضبط السمتَين Variant وConfig على debug. من المرجّح أن يكون ملف تخزين المفاتيح في المجلد الرئيسي ضمن .android/debug.keystore.

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

انسخ تجزئة SHA-1 واملأ الحقل الأخير في مربّع الحوار المشروط لإرسال التطبيق.

إعداد Firebase لنظام التشغيل iOS: خطوات إضافية

افتح ios/Runnder.xcworkspace باستخدام "Xcode". أو مع بيئة تطوير متكاملة (IDE) تختارها.

في VSCode، انقر بزر الماوس الأيمن على المجلد ios/، ثم على open in xcode.

في "استوديو Android"، انقر بزر الماوس الأيمن على مجلد ios/ ثم انقر على flutter متبوعًا بالخيار open iOS module in Xcode.

للسماح بتسجيل الدخول إلى حساب Google على نظام التشغيل iOS، أضِف خيار الضبط CFBundleURLTypes إلى نسخة plist من الملفات. (يُرجى الاطّلاع على مستندات حزمة google_sign_in للحصول على مزيد من المعلومات). في هذه الحالة، يكون الملفان ios/Runner/Info-Debug.plist وios/Runner/Info-Release.plist.

تمت إضافة زوج المفتاح/القيمة من قبل، ولكن يجب استبدال القيم الخاصة بهما:

  1. يمكنك الحصول على قيمة REVERSED_CLIENT_ID من ملف GoogleService-Info.plist، بدون أن يحيط به العنصر <string>..</string>.
  2. استبدِل القيمة في كل من ملف ios/Runner/Info-Debug.plist وios/Runner/Info-Release.plist ضمن المفتاح CFBundleURLTypes.
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

لقد انتهيت الآن من إعداد Firebase.

7. الاستماع إلى آخر الأخبار حول عمليات الشراء

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

الاستماع إلى آخر الأخبار حول عمليات الشراء

في main.dart,، ابحث عن التطبيق المصغّر MyHomePage الذي يتضمّن Scaffold مع BottomNavigationBar يحتوي على صفحتَين. تنشئ هذه الصفحة أيضًا ثلاث قيم Provider لـ DashCounter وDashUpgrades, وDashPurchases. يتتبّع DashCounter العدد الحالي للشرطات ويزيدها تلقائيًا. يدير DashUpgrades الترقيات التي يمكنك شراؤها باستخدام Dashes. يركّز هذا الدرس التطبيقي حول الترميز على DashPurchases.

ويتم تلقائيًا تحديد كائن الموفِّر عند طلب هذا الكائن أول مرة. يرصد هذا العنصر تحديثات الشراء مباشرةً عند بدء تشغيل التطبيق، لذا عليك إيقاف طريقة "التحميل الكسول" في هذا العنصر باستخدام "lazy: false":

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create: (context) => DashPurchases(
    context.read<DashCounter>(),
  ),
  lazy: false,
),

يجب أيضًا توفير نسخة افتراضية من InAppPurchaseConnection. ومع ذلك، لإبقاء التطبيق قابلاً للاختبار، تحتاج إلى طريقة ما لمحاكاة الاتصال. لإجراء ذلك، يمكنك إنشاء طريقة مثيل يمكن تجاوزها في الاختبار وإضافتها إلى السمة main.dart.

lib/main.dart

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

يجب تعديل الاختبار قليلاً إذا أردت مواصلة عمله. راجع widget_test.dart على GitHub للحصول على الرمز الكامل الخاص بـ TestIAPConnection.

test/widget_test.dart

void main() {
  testWidgets('App starts', (WidgetTester tester) async {
    IAPConnection.instance = TestIAPConnection();
    await tester.pumpWidget(const MyApp());
    expect(find.text('Tim Sneath'), findsOneWidget);
  });
}

في lib/logic/dash_purchases.dart، انتقِل إلى رمز DashPurchases ChangeNotifier. في الوقت الحالي، يمكنك إضافة DashCounter فقط إلى الشرطات التي اشتريتها.

أضِف خاصية الاشتراك في البث، _subscription (من النوع StreamSubscription<List<PurchaseDetails>> _subscription;)، وIAPConnection.instance, وعمليات الاستيراد. يجب أن تبحث التعليمة البرمجية الناتجة عن ما يلي:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

class DashPurchases extends ChangeNotifier {
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter);
}

تمت إضافة الكلمة الرئيسية late إلى _subscription بسبب إعداد _subscription في الدالة الإنشائية. تم إعداد هذا المشروع ليكون غير قابل للقيم الفارغة بشكل تلقائي (NNBD)، ما يعني أنّ الخصائص التي لم يتم الإعلان عنها قابلة للقيم الفارغة يجب أن تحتوي على قيمة غير فارغة. يتيح لك مؤهِّل late تأخير تحديد هذه القيمة.

في الدالة الإنشائية، احصل على purchaseUpdatedStream وابدأ الاستماع إلى ساحة المشاركات. باستخدام طريقة dispose()، ألغِ الاشتراك في البث.

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter) {
    final purchaseUpdated =
        iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  Future<void> buy(PurchasableProduct product) async {
    // omitted
  }

  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }
}

يتلقّى التطبيق الآن تحديثات الشراء، لذا ستُجري عملية شراء في القسم التالي.

قبل المتابعة، يمكنك إجراء الاختبارات باستخدام "flutter test"" للتأكّد من أنّه تم إعداد كل شيء بشكل صحيح.

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. إجراء عمليات شراء

في هذا الجزء من الدرس التطبيقي حول الترميز، ستستبدل المنتجات الوهمية الحالية حاليًا بمنتجات حقيقية قابلة للشراء. ويتمّ تحميل هذه المنتجات من المتاجر وعرضها في قائمة، ويتم شراؤها عند النقر على المنتج.

تعديل المنتج القابل للشراء

يعرض PurchasableProduct منتجًا وهميًا. يُرجى تعديله لعرض المحتوى الفعلي من خلال استبدال الصف PurchasableProduct في purchasable_product.dart بالرمز التالي:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus {
  purchasable,
  purchased,
  pending,
}

class PurchasableProduct {
  String get id => productDetails.id;
  String get title => productDetails.title;
  String get description => productDetails.description;
  String get price => productDetails.price;
  ProductStatus status;
  ProductDetails productDetails;

  PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

إزالة عمليات الشراء الوهمية في "dash_purchases.dart," واستبدالها بقائمة فارغة، List<PurchasableProduct> products = [];

تحميل عمليات الشراء المتاحة

لمنح المستخدم إمكانية إجراء عملية شراء، يجب تحميل عمليات الشراء من المتجر. تحقّق أولاً ممّا إذا كان المتجر متوفرًا. عندما لا يكون المتجر متوفّرًا، يؤدي ضبط storeState على notAvailable إلى عرض رسالة خطأ للمستخدم.

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

عندما يكون المتجر متاحًا، يمكنك تحميل عمليات الشراء المتاحة. استنادًا إلى إعداد Firebase السابق، توقَّع ظهور storeKeyConsumable وstoreKeySubscription, وstoreKeyUpgrade. عندما لا تتوفر عملية شراء متوقعة، اطبع هذه المعلومات على وحدة التحكم؛ قد ترغب أيضًا في إرسال هذه المعلومات إلى الخدمة الخلفية.

تعرض الطريقة await iapConnection.queryProductDetails(ids) كلاً من المعرّفات التي لم يتم العثور عليها والمنتجات القابلة للشراء التي تم العثور عليها. استخدِم productDetails في الردّ لتعديل واجهة المستخدم، واضبط StoreState على available.

lib/logic/dash_purchases.dart

import '../constants.dart';

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

استدعِ الدالة loadPurchases() في الدالة الإنشائية:

lib/logic/dash_purchases.dart

  DashPurchases(this.counter) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    loadPurchases();
  }

أخيرًا، غيِّر قيمة الحقل storeState من StoreState.available إلى StoreState.loading:

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

عرض المنتجات القابلة للشراء

ضع في الاعتبار الملف purchase_page.dart. تعرض أداة "PurchasePage" _PurchasesLoading أو _PurchaseList, أو _PurchasesNotAvailable, حسب StoreState. وتعرض الأداة أيضًا عمليات الشراء السابقة للمستخدم، والتي يتم استخدامها في الخطوة التالية.

يعرض التطبيق المصغّر "_PurchaseList" قائمة المنتجات القابلة للشراء ويرسل طلب شراء إلى عنصر "DashPurchases".

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map((product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              }))
          .toList(),
    );
  }
}

من المفترض أن تتمكّن من الاطّلاع على المنتجات المتاحة في متجرَي Android وiOS إذا تم إعدادها بشكلٍ صحيح. يُرجى العِلم أنّ إتاحة عمليات الشراء عند إدخالها في وحدات التحكّم المعنية قد تستغرق بعض الوقت.

ca1a9f97c21e552d.png

ارجع إلى dash_purchases.dart، ونفِّذ الدالة لشراء منتج. ما عليك سوى فصل المواد الاستهلاكية عن السلع غير الاستهلاكية. يُرجى العلم أنّ منتجات الترقية والمنتجات المتوفّرة عند الاشتراك هي منتجات غير استهلاكية.

lib/logic/dash_purchases.dart

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      case storeKeyConsumable:
        await iapConnection.buyConsumable(purchaseParam: purchaseParam);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      default:
        throw ArgumentError.value(
            product.productDetails, '${product.id} is not a known product');
    }
  }

قبل المتابعة، أنشِئ المتغيّر _beautifiedDashUpgrade وعدِّل دالة beautifiedDash للإشارة إليه.

lib/logic/dash_purchases.dart

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;

تتلقّى طريقة _onPurchaseUpdate آخر المعلومات المتعلقة بعملية الشراء، وتعدّل حالة المنتج الذي يظهر في صفحة الشراء، وتطبّق عملية الشراء على منطق الطلب. من المهم الاتصال بالرقم completePurchase بعد معالجة عملية الشراء حتى يتأكد المتجر من إتمام عملية الشراء بشكل صحيح.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

9. إعداد الواجهة الخلفية

قبل الانتقال إلى تتبُّع عمليات الشراء والتحقّق منها، يمكنك إعداد واجهة Dart الخلفية لإتاحة ذلك.

في هذا القسم، اعمل من المجلد dart-backend/ باعتباره الجذر.

تأكد من تثبيت الأدوات التالية:

نظرة عامة على المشروع الأساسي

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

يمكن تشغيل رمز الخلفية هذا محليًا على جهازك، ولن تحتاج إلى نشره لاستخدامه. ومع ذلك، يجب أن تكون قادرًا على الاتصال من جهاز التطوير (Android أو iPhone) بالجهاز الذي سيتم تشغيل الخادم عليه. لإجراء ذلك، يجب أن يكونا متصلَين بالشبكة نفسها، وعليك معرفة عنوان IP لجهازك.

جرب تشغيل الخادم باستخدام الأمر التالي:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

تستخدم الواجهة الخلفية Dart shelf وshelf_router لعرض نقاط نهاية واجهة برمجة التطبيقات. لا يوفر الخادم أي مسارات بشكل تلقائي. سيتم في وقت لاحق إنشاء مسار لمعالجة عملية إثبات ملكية الحساب أثناء عمليات الشراء.

وأحد الأجزاء التي سبق تضمينها في رمز التفعيل هو IapRepository في lib/iap_repository.dart. إنّ تعلُّم كيفية التفاعل مع Firestore، أو قواعد البيانات بشكل عام، ليس ذا صلة بهذا الدرس التطبيقي حول الترميز، لذلك يحتوي الرمز البرمجي للمبتدئين على وظائف تتيح لك إنشاء عمليات الشراء أو تعديلها في Firestore، بالإضافة إلى جميع فئات عمليات الشراء هذه.

إعداد الوصول إلى Firebase

للوصول إلى Firebase Firestore، يجب استخدام مفتاح وصول لحساب الخدمة. أنشئ مفتاحًا لفتح إعدادات مشروع Firebase وانتقِل إلى قسم حسابات الخدمة، ثم اختَر إنشاء مفتاح خاص جديد.

27590fc77ae94ad4.png

انسخ ملف JSON الذي تم تنزيله إلى مجلد "assets/"، ثم أعِد تسميته إلى service-account-firebase.json.

إعداد إمكانية الوصول إلى Google Play

للوصول إلى "متجر Play" لتأكيد عمليات الشراء، يجب إنشاء حساب خدمة باستخدام هذه الأذونات وتنزيل بيانات اعتماد JSON الخاصة به.

  1. انتقِل إلى Google Play Console، وابدأ من صفحة جميع التطبيقات.
  2. الانتقال إلى إعداد > الوصول إلى واجهة برمجة التطبيقات: 317fdfb54921f50e.png إذا طلبت أداة Google Play Console إنشاء مشروع حالي أو الربط بمشروع حالي، عليك إجراء ذلك أولاً ثم العودة إلى هذه الصفحة.
  3. ابحث عن القسم الذي يمكنك من خلاله تحديد حسابات الخدمة، وانقر على إنشاء حساب خدمة جديد.1e70d3f8d794bebb.png
  4. انقر على رابط Google Cloud Platform في مربّع الحوار المنبثق. 7c9536336dd9e9b4.png
  5. اختَر مشروعك. إذا لم يظهر لك هذا الخيار، فتأكد من تسجيل الدخول إلى حساب Google الصحيح ضمن القائمة المنسدلة الحساب في أعلى اليسار. 3fb3a25bad803063.png
  6. بعد اختيار مشروعك، انقر على + إنشاء حساب خدمة في شريط القوائم العلوي. 62fe4c3f8644acd8.png
  7. أدخِل اسمًا لحساب الخدمة، ويمكنك اختياريًا إدخال وصف حتى تتمكّن من تذكُّره والانتقال إلى الخطوة التالية. 8a92d5d6a3dff48c.png
  8. خصِّص حساب الخدمة دور المحرِّر. 6052b7753667ed1a.png
  9. أنهِ المعالج، ثم ارجع إلى صفحة الوصول إلى واجهة برمجة التطبيقات ضمن Play Console، وانقر على إعادة تحميل حسابات الخدمة. من المفترض أن يظهر لك الحساب الجديد في القائمة. 5895a7db8b4c7659.png
  10. انقر على منح إذن الوصول لحساب الخدمة الجديد.
  11. انتقِل إلى أسفل الصفحة التالية وصولاً إلى مجموعة البيانات المالية. اختَر كلاً من عرض البيانات المالية والطلبات والردود على استطلاعات أسباب الإلغاء وإدارة الطلبات والاشتراكات. 75b22d0201cf67e.png
  12. انقر على دعوة مستخدم. 70ea0b1288c62a59.png
  13. الآن وبعد إعداد الحساب، ما عليك سوى إنشاء بعض بيانات الاعتماد. في Cloud Console، ابحث عن حساب الخدمة الخاص بك في قائمة حسابات الخدمة، ثم انقر على النقاط الرأسية الثلاث، واختَر إدارة المفاتيح. 853ee186b0e9954e.png
  14. أنشئ مفتاح JSON جديدًا ونزِّله. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. أعِد تسمية الملف الذي تم تنزيله إلى service-account-google-play.json, وانقله إلى دليل assets/.

هناك إجراء آخر علينا اتّخاذه وهو فتح lib/constants.dart, واستبدال قيمة androidPackageId بمعرّف الحزمة الذي اخترته لتطبيق Android.

إعداد إمكانية الوصول إلى Apple App Store

للوصول إلى App Store من أجل تأكيد عمليات الشراء، عليك إعداد مفتاح سرّي مشترك:

  1. افتح App Store Connect.
  2. انتقِل إلى تطبيقاتي واختَر تطبيقك.
  3. في شريط التنقل الجانبي، انتقِل إلى عمليات الشراء داخل التطبيق >. إدارة:
  4. في أعلى يسار القائمة، انقر على سر مشترك خاص بالتطبيق.
  5. أنشئ مفتاح سرّي جديد وانسخه.
  6. افتح lib/constants.dart, واستبدِل قيمة appStoreSharedSecret بالسر المشترك الذي أنشأته للتو.

d8b8042470aaeff.png

b72f4565750e2f40.png

ملف إعداد العناصر الثابتة

قبل المتابعة، تأكَّد من ضبط الثوابت التالية في ملف lib/constants.dart:

  • androidPackageId: يتم استخدام معرّف الحزمة على Android. مثلاً: com.example.dashclicker
  • appStoreSharedSecret: مفتاح سري مشترك للوصول إلى App Store Connect من أجل إثبات ملكية الحساب أثناء عمليات الشراء
  • bundleId: معرّف الحزمة المستخدَم على iOS. مثلاً: com.example.dashclicker

يمكنك تجاهل باقي الثوابت في الوقت الحالي.

10. تأكيد عمليات الشراء

إنّ المسار العام لتأكيد عمليات الشراء مماثل لنظامَي التشغيل iOS وAndroid.

في ما يتعلق بكلا المتجرَين، يتلقّى تطبيقك رمزًا مميّزًا عند إجراء عملية شراء.

ويرسل التطبيق هذا الرمز المميّز إلى الخدمة الخلفية التي تعمل بدورها على التحقّق من عملية الشراء من خلال خوادم المتجر المعني باستخدام الرمز المميّز المقدَّم.

يمكن لخدمة الخلفية بعد ذلك اختيار تخزين عملية الشراء والرد على التطبيق سواء كانت عملية الشراء صالحة أم لا.

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

بدء إعداد Flutter

إعداد المصادقة

عندما تريد إرسال عمليات الشراء إلى الخدمة الخلفية، عليك التأكد من مصادقة المستخدم أثناء إجراء عملية شراء. سبق أن تمت إضافة معظم منطق المصادقة بالنيابة عنك في مشروع إجراء التفعيل، وما عليك سوى التأكّد من أنّ PurchasePage يعرض زر تسجيل الدخول عندما يكون المستخدم غير مسجّل الدخول بعد. أضِف الرمز التالي إلى بداية طريقة إنشاء PurchasePage:

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

class PurchasePage extends StatelessWidget {  
  const PurchasePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }
    // omitted

نقطة نهاية التحقّق من المكالمات من التطبيق

في التطبيق، أنشِئ الدالة _verifyPurchase(PurchaseDetails purchaseDetails) التي تستدعي نقطة النهاية /verifypurchase على خلفية Dart باستخدام مكالمة http بعد انتهاء العملية.

أرسِل المتجر الذي اخترته (google_play مقابل "متجر Play" أو app_store لتطبيق App Store) وserverVerificationData وproductID. يعرض الخادم رمز الحالة الذي يشير إلى ما إذا تم تأكيد عملية الشراء.

في ثوابت التطبيق، يمكنك ضبط عنوان IP للخادم على عنوان IP لجهازك المحلي.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

  DashPurchases(this.counter, this.firebaseNotifier) {
    // omitted
  }

إضافة firebaseNotifier مع إنشاء DashPurchases في main.dart:

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
          ),
          lazy: false,
        ),

أضِف قيمة getter للمستخدم في FirebaseNotifier، حتى تتمكّن من تمرير رقم تعريف المستخدم إلى وظيفة "التحقُّق من الشراء".

lib/logic/firebase_notifier.dart

  User? get user => FirebaseAuth.instance.currentUser;

أضِف الدالة _verifyPurchase إلى الفئة DashPurchases. تعرض الدالة async هذه قيمة منطقية تشير إلى ما إذا تم التحقّق من صحة عملية الشراء.

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

يمكنك طلب الدالة _verifyPurchase في _handlePurchase قبل تطبيق عملية الشراء مباشرةً. يجب عدم تطبيق عملية الشراء إلا بعد إثبات ملكيتها. وفي تطبيق علني، يمكنك تحديد ذلك أيضًا لتطبيق اشتراك تجريبي عندما يكون المتجر غير متاح مؤقتًا مثلاً. مع ذلك، في هذا المثال، اجعل عملية الشراء بسيطة، ولا تطبِّق عملية الشراء إلا بعد إثبات ملكية عملية الشراء بنجاح.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      // Send to server
      var validPurchase = await _verifyPurchase(purchaseDetails);

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

أصبح كل شيء في التطبيق جاهزًا الآن للتحقق من عمليات الشراء.

إعداد خدمة الخلفية

بعد ذلك، قم بإعداد وظيفة السحابة للتحقق من عمليات الشراء في الخلفية.

إنشاء معالِجات الشراء

بما أنّ خطوات إثبات الملكية في كلا المتجرَين متقاربة، يجب إعداد فئة PurchaseHandler مختصرة مع عمليات تنفيذ منفصلة لكلّ متجر.

be50c207c5a2a519.png

ابدأ بإضافة ملف purchase_handler.dart إلى المجلد lib/، حيث يمكنك تحديد صف PurchaseHandler موجز بطريقتَين مختصرتَين للتحقّق من نوعَين مختلفَين من عمليات الشراء: الاشتراكات وغير الاشتراكات.

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {

  /// Verify if non-subscription purchase (aka consumable) is valid
  /// and update the database
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });

  /// Verify if subscription purchase (aka non-consumable) is valid
  /// and update the database
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });
}

كما ترى، تتطلب كل طريقة ثلاث معلمات:

  • userId: رقم تعريف المستخدم الذي سجَّل الدخول، حتى تتمكّن من ربط عمليات الشراء بالمستخدم
  • productData: بيانات عن المنتج ستحدد ذلك خلال دقيقة.
  • token: الرمز المميّز الذي وفّره المتجر للمستخدم

ولتسهيل استخدام معالِجات الشراء هذه، يمكنك أيضًا إضافة طريقة verifyPurchase() يمكن استخدامها للاشتراكات وغير المرتبطة بالاشتراكات:

lib/purchase_handler.dart

  /// Verify if purchase is valid and update the database
  Future<bool> verifyPurchase({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    switch (productData.type) {
      case ProductType.subscription:
        return handleSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
      case ProductType.nonSubscription:
        return handleNonSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
    }
  }

يمكنك الآن فقط طلب الرقم verifyPurchase لكلتا الحالتين، ولكن لا يزال هناك عمليات تنفيذ منفصلة.

تحتوي الفئة ProductData على معلومات أساسية حول المنتجات المختلفة التي يمكن شراؤها، بما في ذلك معرّف المنتج (ويُشار إليه أحيانًا أيضًا باسم رمز التخزين التعريفي) وProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

  const ProductData(this.productId, this.type);
}

ويمكن أن يكون ProductType اشتراكًا أو غير اشتراك.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

أخيرًا، يتم تعريف قائمة المنتجات كخريطة في الملف نفسه.

lib/products.dart

const productDataMap = {
  'dash_consumable_2k': ProductData(
    'dash_consumable_2k',
    ProductType.nonSubscription,
  ),
  'dash_upgrade_3d': ProductData(
    'dash_upgrade_3d',
    ProductType.nonSubscription,
  ),
  'dash_subscription_doubler': ProductData(
    'dash_subscription_doubler',
    ProductType.subscription,
  ),
};

بعد ذلك، حدِّد بعض عمليات تنفيذ العناصر النائبة لـ "متجر Google Play" وApple App Store. البدء بـ Google Play:

يمكنك إنشاء "lib/google_play_purchase_handler.dart" وإضافة صف يوسِّع نطاق PurchaseHandler الذي كتبته للتو:

lib/google_play_purchase_handler.dart

import 'dart:async';

import 'package:googleapis/androidpublisher/v3.dart' as ap;

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
  );

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }
}

وفي الوقت الحالي، تعرض الدالة true لطرق المعالجات؛ ستصل إليها لاحقًا.

وكما لاحظت، تأخذ الدالة الإنشائية مثيلاً للدالة IapRepository. يستخدم معالج الشراء هذا المثيل لتخزين معلومات عن عمليات الشراء في Firestore لاحقًا. للتواصل مع Google Play، يمكنك استخدام AndroidPublisherApi المقدّم.

بعد ذلك، افعل الشيء نفسه مع معالج متجر التطبيقات. أنشِئ lib/app_store_purchase_handler.dart، ثم أضِف فئة توسّع PurchaseHandler مرة أخرى:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return true;
  }
}

رائع! الآن لديك معالجان للشراء. بعد ذلك، سننشئ نقطة نهاية واجهة برمجة التطبيقات لتأكيد عمليات الشراء.

استخدام معالِجات الشراء

افتح bin/server.dart وأنشئ نقطة نهاية لواجهة برمجة التطبيقات باستخدام shelf_route:

bin/server.dart

Future<void> main() async {
  final router = Router();

  final purchaseHandlers = await _createPurchaseHandlers();

  router.post('/verifypurchase', (Request request) async {
    final dynamic payload = json.decode(await request.readAsString());

    final (:userId, :source, :productData, :token) = getPurchaseData(payload);

    final result = await purchaseHandlers[source]!.verifyPurchase(
      userId: userId,
      productData: productData,
      token: token,
    );

    if (result) {
      return Response.ok('all good!');
    } else {
      return Response.internalServerError();
    }
  });

  await serveHandler(router);
}

({
  String userId,
  String source,
  ProductData productData,
  String token,
}) getPurchaseData(dynamic payload) {
  if (payload
      case {
        'userId': String userId,
        'source': String source,
        'productId': String productId,
        'verificationData': String token,
      }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

يقوم الرمز البرمجي أعلاه بما يلي:

  1. حدد نقطة نهاية POST التي سيتم استدعاؤها من التطبيق الذي أنشأته سابقًا.
  2. فك ترميز حمولة JSON واستخراج المعلومات التالية:
  3. userId: رقم تعريف المستخدم المسجَّل الدخول إليه حاليًا
  4. source: تم استخدام المتجر، إما app_store أو google_play.
  5. productData: تم الحصول عليه من productDataMap الذي أنشأته سابقًا.
  6. token: يحتوي على بيانات إثبات الهوية لإرسالها إلى المتاجر.
  7. يمكنك استدعاء طريقة verifyPurchase إما للسمة GooglePlayPurchaseHandler أو AppStorePurchaseHandler، اعتمادًا على المصدر.
  8. إذا تم إثبات الملكية بنجاح، ستعرض الطريقة رمز الاستجابة Response.ok للعميل.
  9. إذا تعذَّر إثبات الملكية، تعرض الطريقة رمز الاستجابة Response.internalServerError للعميل.

بعد إنشاء نقطة نهاية واجهة برمجة التطبيقات، ستحتاج إلى ضبط معالِجات الشراء. لهذا السبب، عليك تحميل مفاتيح حساب الخدمة التي حصلت عليها في الخطوة السابقة وضبط أذونات الوصول إلى الخدمات المختلفة، بما في ذلك Android publisher API وFirebase Firestore API. بعد ذلك، قم بإنشاء معالجي الشراء بتبعيات مختلفة:

bin/server.dart

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
  // Configure Android Publisher API access
  final serviceAccountGooglePlay =
      File('assets/service-account-google-play.json').readAsStringSync();
  final clientCredentialsGooglePlay =
      auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
  final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
  ]);
  final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

  // Configure Firestore API access
  final serviceAccountFirebase =
      File('assets/service-account-firebase.json').readAsStringSync();
  final clientCredentialsFirebase =
      auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
  final clientFirebase =
      await auth.clientViaServiceAccount(clientCredentialsFirebase, [
    fs.FirestoreApi.cloudPlatformScope,
  ]);
  final firestoreApi = fs.FirestoreApi(clientFirebase);
  final dynamic json = jsonDecode(serviceAccountFirebase);
  final projectId = json['project_id'] as String;
  final iapRepository = IapRepository(firestoreApi, projectId);

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };
}

تأكيد عمليات الشراء من Android: تنفيذ معالج الشراء

بعد ذلك، يمكنك مواصلة تنفيذ معالِج الشراء في Google Play.

توفّر Google حاليًا حزم Dart للتفاعل مع واجهات برمجة التطبيقات التي تحتاجها لتأكيد عمليات الشراء. لقد أعددتها في ملف server.dart وتستخدمها الآن في الصف GooglePlayPurchaseHandler.

تنفيذ المعالج لعمليات الشراء غير المرتبطة باشتراكات:

lib/google_play_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

يمكنك تعديل معالِج شراء الاشتراكات بطريقة مماثلة:

lib/google_play_purchase_handler.dart

  /// Handle subscription purchases.
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.subscriptions.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Subscription response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = extractOrderId(response.orderId!);

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

أضِف الطريقة التالية لتسهيل تحليل مُعرّفات الطلبات، بالإضافة إلى طريقتَين لتحليل حالة الشراء.

lib/google_play_purchase_handler.dart

/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
  final orderIdSplit = orderId.split('..');
  if (orderIdSplit.isNotEmpty) {
    orderId = orderIdSplit[0];
  }
  return orderId;
}

NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
  return switch (state) {
    0 => NonSubscriptionStatus.completed,
    2 => NonSubscriptionStatus.pending,
    _ => NonSubscriptionStatus.cancelled,
  };
}

SubscriptionStatus _subscriptionStatusFrom(int? state) {
  return switch (state) {
    // Payment pending
    0 => SubscriptionStatus.pending,
    // Payment received
    1 => SubscriptionStatus.active,
    // Free trial
    2 => SubscriptionStatus.active,
    // Pending deferred upgrade/downgrade
    3 => SubscriptionStatus.pending,
    // Expired or cancelled
    _ => SubscriptionStatus.expired,
  };
}

من المفترض الآن أن يتم إثبات ملكية عمليات الشراء التي أجريتها على Google Play وتخزينها في قاعدة البيانات.

بعد ذلك، انتقِل إلى عمليات الشراء في App Store على أجهزة iOS.

التأكّد من عمليات الشراء على أجهزة iOS: تنفيذ معالِج الشراء

لتأكيد عمليات الشراء باستخدام App Store، تتوفّر حزمة Dart تابعة لجهة خارجية اسمها app_store_server_sdk، ما يسهّل هذه العملية.

ابدأ بإنشاء مثيل ITunesApi. استخدام تهيئة وضع الحماية، فضلاً عن تفعيل التسجيل لتسهيل تصحيح الأخطاء.

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(
      ITunesEnvironment.sandbox(),
      loggingEnabled: true,
    ),
  );

وعلى عكس واجهات برمجة تطبيقات Google Play، يستخدم متجر App Store الآن نقاط نهاية واجهة برمجة التطبيقات نفسها لكلّ من الاشتراكات وغير الاشتراكات. وهذا يعني أنه يمكنك استخدام المنطق نفسه لكلا المعالجين. ادمجهما معًا لاستدعاء عملية التنفيذ نفسها:

lib/app_store_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
   //..
  }

الآن، نفِّذ handleValidation:

lib/app_store_purchase_handler.dart

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
    print('AppStorePurchaseHandler.handleValidation');
    final response = await _iTunesAPI.verifyReceipt(
      password: appStoreSharedSecret,
      receiptData: token,
    );
    print('response: $response');
    if (response.status == 0) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              status: NonSubscriptionStatus.completed,
            ));
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0')),
              status: SubscriptionStatus.active,
            ));
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

من المفترض الآن أن يتم التحقق من عمليات الشراء التي تم إجراؤها في App Store وتخزينها في قاعدة البيانات.

تشغيل الخلفية

في هذه المرحلة، يمكنك تشغيل dart bin/server.dart لعرض نقطة النهاية /verifypurchase.

$ dart bin/server.dart 
Serving at http://0.0.0.0:8080

11. تتبُّع عمليات الشراء

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

أولاً، عليك إعداد معالجة أحداث المتجر في الخلفية باستخدام واجهة Dart التي كنت تبنيها.

معالجة أحداث المتجر في الخلفية

يمكن للمتاجر إعلام الخلفية بأي أحداث فوترة تحدث، مثلاً عند تجديد الاشتراكات. يمكنك معالجة هذه الأحداث في الخلفية لتحديث عمليات الشراء في قاعدة البيانات لديك. في هذا القسم، يمكنك إعداد هذا لكل من "متجر Google Play" وApple App Store.

معالجة أحداث "الفوترة في Google Play"

يوفّر Google Play أحداث الفوترة من خلال ما يُطلق عليه موضوع النشر/الفرع في السحابة الإلكترونية. وتكون هذه في الأساس قوائم انتظار للرسائل يمكن نشر الرسائل عليها وكذلك استهلاكها.

ولأنّ هذه الوظيفة خاصة بـ Google Play، يجب تضمين هذه الوظيفة في GooglePlayPurchaseHandler.

ابدأ بفتح lib/google_play_purchase_handler.dart، وإضافة عملية استيراد PubsubApi:

lib/google_play_purchase_handler.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;

بعد ذلك، أدخِل PubsubApi إلى GooglePlayPurchaseHandler وعدِّل الدالة الإنشائية للفئة لإنشاء Timer على النحو التالي:

lib/google_play_purchase_handler.dart

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;
  final pubsub.PubsubApi pubsubApi; // new

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
    this.pubsubApi, // new
  ) {
    // Poll messages from Pub/Sub every 10 seconds
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullMessageFromPubSub();
    });
  }

تم ضبط Timer لاستدعاء الطريقة _pullMessageFromSubSub كل عشر ثوانٍ. يمكنك تعديل المدة وفقًا لتفضيلك الخاص.

بعد ذلك، عليك إنشاء _pullMessageFromSubSub.

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(
      maxMessages: 1000,
    );
    final topicName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(
      ackIds: [id],
    );
    final subscriptionName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

يتواصل الرمز الذي أضفته للتو مع موضوع النشر/الاشتراك من Google Cloud كل عشر ثوانٍ ويطلب رسائل جديدة. بعد ذلك، تتم معالجة كل رسالة بطريقة _processMessage.

تعمل هذه الطريقة على فك ترميز الرسائل الواردة والحصول على المعلومات المحدَّثة حول كل عملية شراء، سواء كانت اشتراكات أو اشتراكات أخرى، من خلال الاتصال بالرقم handleSubscription أو handleNonSubscription الحالي إذا لزم الأمر.

يجب الإقرار بكل رسالة باستخدام الطريقة _askMessage.

أضِف بعد ذلك الملحقات المطلوبة إلى ملف server.dart. أضِف PubsubApi.cloudPlatformScope إلى إعدادات بيانات الاعتماد:

bin/server.dart

 final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
    pubsub.PubsubApi.cloudPlatformScope, // new
  ]);

بعد ذلك، أنشئ مثيل PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

وأخيرًا، ضعه في الدالة الإنشائية GooglePlayPurchaseHandler:

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi, // new
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

إعداد Google Play

لقد كتبت الرمز لاستخدام أحداث الفوترة من موضوع الناشر/الموضوع الفرعي، ولكنك لم تنشئ موضوع الناشر/الموضوع الفرعي، ولا تنشر أي أحداث فوترة. حان الوقت لإعداد هذه الميزة.

أولاً، أنشئ موضوعًا عامًا أو فرعيًا:

  1. انتقِل إلى صفحة Cloud Pub/Sub على Google Cloud Console.
  2. تأكَّد من أنّك في مشروعك على Firebase، ثم انقر على + إنشاء موضوع. d5ebf6897a0a8bf5.png
  3. أدخِل اسمًا للموضوع الجديد، مطابقًا للقيمة المحددة لـ GOOGLE_PLAY_PUBSUB_BILLING_TOPIC في constants.ts. وفي هذه الحالة، يمكنك تسميته play_billing. إذا اخترت إعدادات أخرى، احرص على تعديل "constants.ts". أنشئ الموضوع. 20d690fc543c4212.png
  4. في قائمة مواضيع الناشر/الفرعي، انقر على النقاط الرأسية الثلاث للموضوع الذي أنشأته للتو، ثم انقر على عرض الأذونات. ea03308190609fb.png
  5. في الشريط الجانبي على يسار الصفحة، اختَر إضافة مدير.
  6. في هذا القسم، أضِف google-play-developer-notifications@system.gserviceaccount.com وامنحه دور الناشر/الناشر. 55631ec0549215bc.png
  7. احفظ تغييرات الأذونات.
  8. انسخ اسم الموضوع للموضوع الذي أنشأته للتو.
  9. افتح Play Console مرة أخرى واختَر تطبيقك من قائمة جميع التطبيقات.
  10. مرِّر لأسفل وانتقِل إلى تحقيق الربح > إعداد تحقيق الربح:
  11. املأ الموضوع بالكامل واحفظ التغييرات. 7e5e875dc6ce5d54.png

سيتم الآن نشر جميع أحداث "الفوترة في Google Play" حول هذا الموضوع.

معالجة أحداث الفوترة في App Store

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

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

في بيئة إنتاج، يُفضَّل أن يكون لديكما الاثنين معًا. الردّ التلقائي على الويب للحصول على الأحداث من App Store وServer API في حال تفويت حدث أو كنت بحاجة إلى التحقّق جيدًا من حالة الاشتراك.

ابدأ بفتح lib/app_store_purchase_handler.dart، وإضافة الاعتمادية على AppStoreServerAPI:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

AppStorePurchaseHandler(
  this.iapRepository,
  this.appStoreServerAPI, // new
)

عدِّل الدالة الإنشائية لإضافة مؤقت سيتم الاتصال بطريقة _pullStatus. سيطلب هذا الموقّت استخدام طريقة _pullStatus كل 10 ثوانٍ. يمكنك تعديل مدة الموقّت حسب احتياجاتك.

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,
  ) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

بعد ذلك، قم بإنشاء طريقة _pullStatus على النحو التالي:

lib/app_store_purchase_handler.dart

  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where((element) =>
        element.type == ProductType.subscription &&
        element.iapSource == IAPSource.appstore);
    for (final purchase in appStoreSubscriptions) {
      final status =
          await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
      // Obtain all subscriptions for the order id.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
              transaction.transactionInfo.expiresDate ?? 0);
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(SubscriptionPurchase(
            userId: null,
            productId: transaction.transactionInfo.productId,
            iapSource: IAPSource.appstore,
            orderId: transaction.originalTransactionId,
            purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate),
            type: ProductType.subscription,
            expiryDate: expirationDate,
            status: isExpired
                ? SubscriptionStatus.expired
                : SubscriptionStatus.active,
          ));
        }
      }
    }
  }

تعمل هذه الطريقة على النحو التالي:

  1. الحصول على قائمة بالاشتراكات النشطة من Firestore باستخدام IapRepository
  2. بالنسبة إلى كل طلب، يطلب التطبيق حالة الاشتراك من App Store Server API.
  3. الحصول على آخر معاملة لعملية شراء الاشتراك هذه
  4. التحقق من تاريخ انتهاء الصلاحية.
  5. لتعديل حالة الاشتراك على Firestore، إذا انتهت صلاحيته، سيتم وضع علامة على هذه الحالة تشير إلى ذلك.

أخيرًا، أضِف كل الرموز اللازمة لضبط الوصول إلى واجهة برمجة تطبيقات App Store Server:

bin/server.dart

  // add from here
  final subscriptionKeyAppStore =
      File('assets/SubscriptionKey.p8').readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here


  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
      appStoreServerAPI, // new
    ),
  };

إعداد App Store

بعد ذلك، عليك إعداد App Store:

  1. سجِّل الدخول إلى App Store Connect، وانقر على المستخدمون وأذونات الوصول.
  2. انتقل إلى نوع المفتاح > عملية شراء داخل التطبيق:
  3. انقر على علامة الجمع لإضافة رمز جديد.
  4. أدخِل اسمًا لها، على سبيل المثال: "مفتاح درس تطبيقي حول الترميز".
  5. نزِّل ملف p8 الذي يحتوي على المفتاح.
  6. انسخه إلى مجلد مواد العرض الذي يحمل الاسم SubscriptionKey.p8.
  7. انسخ رقم تعريف المفتاح من المفتاح الذي تم إنشاؤه حديثًا واضبطه على ثابت appStoreKeyId في ملف lib/constants.dart.
  8. انسخ رقم تعريف جهة الإصدار مباشرةً أعلى قائمة المفاتيح، واضبطه على قيمة ثابتة واحدة (appStoreIssuerId) في ملف lib/constants.dart.

9540ea9ada3da151.png

تتبُّع عمليات الشراء على الجهاز

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

سبق أن ضمّنت IAPRepo في التطبيق، وهو مستودع Firestore الذي يحتوي على جميع بيانات الشراء الخاصة بالمستخدم في "List<PastPurchase> purchases". يحتوي المستودع أيضًا على hasActiveSubscription,، وهذا صحيح عند إجراء عملية شراء باستخدام productId storeKeySubscription بحالة غير منتهية الصلاحية. في حال عدم تسجيل المستخدم الدخول، تكون القائمة فارغة.

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((DocumentSnapshot document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

      hasActiveSubscription = purchases.any((element) =>
          element.productId == storeKeySubscription &&
          element.status != Status.expired);

      hasUpgrade = purchases.any(
        (element) => element.productId == storeKeyUpgrade,
      );

      notifyListeners();
    });
  }

تندرج كل عمليات الشراء المنطقية في الفئة DashPurchases وهي المكان الذي يجب فيه تطبيق الاشتراكات أو إزالتها. إذًا، أضِف iapRepo كخاصية في الفئة وحدِّد iapRepo في الدالة الإنشائية. بعد ذلك، أضف مستمعًا مباشرةً في الدالة الإنشائية، وأزِل المستمع في طريقة dispose(). في البداية، يمكن أن يكون المستمع مجرد دالة فارغة. بما أنّ IAPRepo هي ChangeNotifier وتطلب منك notifyListeners() في كل مرة تتغيّر فيها عمليات الشراء في Firestore، يتم دائمًا طلب طريقة purchasesUpdate() عند تغيير المنتجات التي تم شراؤها.

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

  DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
    final purchaseUpdated =
        iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    iapRepo.addListener(purchasesUpdate);
    loadPurchases();
  }

  @override
  void dispose() {
    iapRepo.removeListener(purchasesUpdate);
    _subscription.cancel();
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

بعد ذلك، يمكنك توفير IAPRepo إلى الدالة الإنشائية في main.dart.. يمكنك الحصول على المستودع باستخدام context.read لأنه تم إنشاؤه من قبل في Provider.

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),
          ),
          lazy: false,
        ),

بعد ذلك، اكتب التعليمة البرمجية للدالة purchaseUpdate(). في dash_counter.dart,، يتم ضبط المُضاعِف على 10 أو 1 في الطريقتَين applyPaidMultiplier وremovePaidMultiplier، على التوالي، كي لا تضطر إلى التحقّق مما إذا كان الاشتراك مطبَّقًا حاليًا. وعند تغيّر حالة الاشتراك، يمكنك أيضًا تعديل حالة المنتج القابل للشراء لإظهار أنّه نشط في صفحة الشراء. اضبط السمة _beautifiedDashUpgrade استنادًا إلى ما إذا كان يتم شراء الترقية.

lib/logic/dash_purchases.dart

void purchasesUpdate() {
    var subscriptions = <PurchasableProduct>[];
    var upgrades = <PurchasableProduct>[];
    // Get a list of purchasable products for the subscription and upgrade.
    // This should be 1 per type.
    if (products.isNotEmpty) {
      subscriptions = products
          .where((element) => element.productDetails.id == storeKeySubscription)
          .toList();
      upgrades = products
          .where((element) => element.productDetails.id == storeKeyUpgrade)
          .toList();
    }

    // Set the subscription in the counter logic and show/hide purchased on the
    // purchases page.
    if (iapRepo.hasActiveSubscription) {
      counter.applyPaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchased);
      }
    } else {
      counter.removePaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchasable);
      }
    }

    // Set the Dash beautifier and show/hide purchased on
    // the purchases page.
    if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
      _beautifiedDashUpgrade = iapRepo.hasUpgrade;
      for (var element in upgrades) {
        _updateStatus(
          element,
          _beautifiedDashUpgrade
              ? ProductStatus.purchased
              : ProductStatus.purchasable);
      }
      notifyListeners();
    }
  }

  void _updateStatus(PurchasableProduct product, ProductStatus status) {
    if (product.status != ProductStatus.purchased) {
      product.status = ProductStatus.purchased;
      notifyListeners();
    }
  }

لقد تأكّدت الآن من أنّ حالة الاشتراك والترقية محدَّثة دائمًا في خدمة الخلفية وأنّه تمت مزامنته مع التطبيق. يعمل التطبيق وفقًا لذلك ويطبِّق ميزات الاشتراك والترقية على لعبة النقر في Dash.

12. أكملت كل الإجراءات

تهانينا! لقد أكملت الدرس التطبيقي حول الترميز. يمكنك العثور على الرمز المكتمل لهذا الدرس التطبيقي في android_studio_مجلد.pngالمجلد الكامل.

لمزيد من المعلومات، يمكنك تجربة الدروس التطبيقية حول ترميز Flutter.