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

1. مقدمة

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

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

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

يمنح خيار الشراء الأول المستخدم فائدة مباشرة تبلغ 2,000 Dash. وهي متاحة للمستخدم مباشرةً ويمكن شراؤها عدة مرات. يُطلق على هذا المنتج اسم "منتج قابل للاستهلاك" لأنّه يُستهلك مباشرةً ويمكن استهلاكه عدة مرات.

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

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

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

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

ما ستنشئه

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

ما ستتعرّف عليه

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

المتطلبات

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

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

تنزيل الرمز

لاستنساخ مستودع GitHub من سطر الأوامر، استخدِم الأمر التالي:

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

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

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/app في بيئة التطوير المتكاملة المفضّلة لديك. استخدمنا "استوديو Android" للقطات الشاشة، ولكنّ Visual Studio Code هو أيضًا خيار رائع. تأكَّد من تثبيت أحدث إصدار من إضافتَي Dart وFlutter في أي من المحرِّرين.

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

أولاً، عليك إعداد معرّف الحزمة لنظام التشغيل iOS. لإجراء ذلك، افتح ملف Runner.xcworkspace في تطبيق Xcode.

a9fbac80a31e28e0.png

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

812f919d965c649a.jpeg

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

e320a49ff2068ac2.png

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

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

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

أضِف in_app_purchase إلى ملف pubspec من خلال إضافة in_app_purchase إلى التبعيات في مشروعك:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

افتح pubspec.yaml وتأكَّد من أنّ in_app_purchase مدرَج الآن كإدخال ضمن dependencies، وأنّ in_app_purchase_platform_interface مدرَج ضمن dev_dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  in_app_purchase_platform_interface: ^1.4.0

4. إعداد App Store

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

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

11db9fca823e7608.png

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

74c73197472c9aec.png

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

4a100bbb8cafdbbf.jpeg

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

أنشِئ معرّفًا جديدًا في Apple Developer Portal. انتقِل إلى developer.apple.com/account/resources/identifiers/list وانقر على رمز الإضافة بجانب العنوان المعرّفات.

55d7e592d9a3fc7b.png

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

13f125598b72ca77.png

اختيار تطبيق

41ac4c13404e2526.png

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

9d2c940ad80deeef.png

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

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

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

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

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

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

2ba0f599bcac9b36.png

يمكنك الآن إعداد مستخدم وضع الحماية على جهاز iPhone من خلال الانتقال إلى الإعدادات > المطوّر > حساب Apple في وضع الحماية.

74a545210b282ad8.png eaa67752f2350f74.png

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

ستضبط الآن إعدادات المنتجات الثلاثة القابلة للشراء:

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

a118161fac83815a.png

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

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

  1. اضبط dash_consumable_2k كـ منتج للاستهلاك. استخدِم dash_consumable_2k كمعرّف المنتج. يُستخدم الاسم المرجعي في App Store Connect فقط، ما عليك سوى ضبطه على dash consumable 2k. 1f8527fc03902099.png إعداد مدى التوفّر يجب أن يكون المنتج متاحًا في بلد مستخدم البيئة التجريبية. bd6b2ce2d9314e6e.png أضِف السعر واضبطه على $1.99 أو ما يعادله بعملة أخرى. 926b03544ae044c4.png أضِف عمليات الترجمة إلى اللغات المحلية لعملية الشراء. أضِف عملية الشراء Spring is in the air مع استخدام 2000 dashes fly out كوصف. e26dd4f966dcfece.png أضِف لقطة شاشة للمراجعة. لا يهم المحتوى إلا إذا تم إرسال المنتج للمراجعة، ولكنّه مطلوب لكي يكون المنتج في الحالة "جاهز للإرسال"، وهو أمر ضروري عندما يجلب التطبيق المنتجات من App Store. 25171bfd6f3a033a.png
  2. إعداد dash_upgrade_3d كمنتج غير قابل للاستهلاك استخدِم dash_upgrade_3d كمعرّف المنتج. اضبط اسم المرجع على dash upgrade 3d. أضِف عملية الشراء 3D Dash مع استخدام Brings your dash back to the future كوصف. اضبط السعر على $0.99. اضبط مدى التوفّر وحمِّل لقطة شاشة المراجعة بالطريقة نفسها التي اتّبعتها مع المنتج dash_consumable_2k. 83878759f32a7d4a.png
  3. إعداد dash_subscription_doubler كـ اشتراك يتجدّد تلقائيًا تختلف خطوات الاشتراك قليلاً. عليك أولاً إنشاء مجموعة اشتراكات. عندما تكون اشتراكات متعدّدة جزءًا من المجموعة نفسها، يمكن للمستخدم الاشتراك في أحدها فقط في الوقت نفسه، ولكن يمكنه الترقية أو الرجوع إلى إصدار أقدم من هذه الاشتراكات. ما عليك سوى الاتصال بهذه المجموعة subscriptions. 393a44b09f3cd8bf.png أضِف ترجمة إلى لغات أخرى لمجموعة الاشتراكات. 595aa910776349bd.png بعد ذلك، عليك إنشاء الاشتراك. اضبط "اسم المرجع" على dash subscription doubler و"معرّف المنتج" على dash_subscription_doubler. 7bfff7bbe11c8eec.png بعد ذلك، اختَر مدة الاشتراك لمدة أسبوع واحد واللغات. أطلِق على هذا الاشتراك الاسم Jet Engine مع الوصف Doubles your clicks. اضبط السعر على $0.49. اضبط مدى التوفّر وحمِّل لقطة شاشة المراجعة بالطريقة نفسها التي اتّبعتها مع المنتج dash_consumable_2k. 44d18e02b926a334.png

من المفترض أن تظهر لك المنتجات الآن في القوائم:

17f242b5c1426b79.png d71da951f595054a.png

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

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

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

أنشئ تطبيقًا جديدًا في Google Play Console باتّباع الخطوات التالية:

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

بعد إنشاء تطبيقك، انتقِل إلى لوحة البيانات وأكمِل جميع المهام في قسم إعداد تطبيقك. في هذه الصفحة، يمكنك تقديم بعض المعلومات عن تطبيقك، مثل التقييمات حسب الفئة العمرية ولقطات الشاشة. 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

يجب أن يكون الملف خاصًا، لذا لا تضعه في نظام إدارة الإصدارات المتاح للجميع.

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

أنشِئ ملفًا باسم <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.kts.

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

import java.util.Properties
import java.io.FileInputStream

plugins {
    // omitted
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // omitted
}

حمِّل ملف key.properties في العنصر keystoreProperties.

عدِّل حظر buildTypes إلى:

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

   signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

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

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

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

flutter build appbundle

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

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

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

انقر على حفظ، ثم انقر على مراجعة الإصدار.

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

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

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

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

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

a0d0394e85128f84.png

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

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

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

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

a1a0f9d3e55ea8da.png

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

الآن، عليك ضبط المنتجات التي يمكن شراؤها داخل التطبيق.

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

  • dash_consumable_2k: عملية شراء استهلاكية يمكن إجراؤها عدة مرات، وتمنح المستخدم 2, 000 من عملة Dashes (العملة داخل التطبيق) لكل عملية شراء.
  • dash_upgrade_3d: عملية شراء "ترقية" غير قابلة للاستهلاك يمكن شراؤها مرة واحدة فقط، وتمنح المستخدم Dash مختلفًا من الناحية الشكلية للنقر عليه.
  • 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.

fe2e0933d6810888.png

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

d02d641821c71e2c.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. اتّبِع التعليمات كما هو موضّح في صفحة الإعداد.

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

$ 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، واملأ الحقل الأخير في مربّع الحوار المشروط الخاص بإرسال التطبيق.

أخيرًا، شغِّل الأمر flutterfire configure مرة أخرى لتعديل التطبيق وتضمين إعدادات التوقيع.

$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes

إعداد Firebase على أجهزة iOS: خطوات إضافية

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

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

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

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

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

  1. احصل على قيمة REVERSED_CLIENT_ID من الملف GoogleService-Info.plist، بدون العنصر <string>..</string> المحيط بها.
  2. استبدِل القيمة في ملف ios/Runner/Info.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 يتضمّن صفحتَين. تنشئ هذه الصفحة أيضًا ثلاث Providers لكل من DashCounter وDashUpgrades, وDashPurchases. يتتبّع DashCounter العدد الحالي من علامات الشرطة ويزيدها تلقائيًا. تتولّى DashUpgrades إدارة الترقيات التي يمكنك شراؤها باستخدام عملات Dashes. يركّز هذا الدرس التطبيقي حول الترميز على DashPurchases.

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

lib/main.dart

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

تحتاج أيضًا إلى نسخة من 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!;
  }
}

عدِّل الاختبار على النحو التالي:

test/widget_test.dart

import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import

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

class TestIAPConnection implements InAppPurchase {         // Add from here
  @override
  Future<bool> buyConsumable({
    required PurchaseParam purchaseParam,
    bool autoConsume = true,
  }) {
    return Future.value(false);
  }

  @override
  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
    return Future.value(false);
  }

  @override
  Future<void> completePurchase(PurchaseDetails purchase) {
    return Future.value();
  }

  @override
  Future<bool> isAvailable() {
    return Future.value(false);
  }

  @override
  Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
    return Future.value(
      ProductDetailsResponse(productDetails: [], notFoundIDs: []),
    );
  }

  @override
  T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
    // TODO: implement getPlatformAddition
    throw UnimplementedError();
  }

  @override
  Stream<List<PurchaseDetails>> get purchaseStream =>
      Stream.value(<PurchaseDetails>[]);

  @override
  Future<void> restorePurchases({String? applicationUserName}) {
    // TODO: implement restorePurchases
    throw UnimplementedError();
  }

  @override
  Future<String> countryCode() {
    // TODO: implement countryCode
    throw UnimplementedError();
  }
}                                                          // To here.

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

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

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';           // Add this import

import '../main.dart';                                           // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.available;
  late StreamSubscription<List<PurchaseDetails>> _subscription;  // Add this line
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;                  // And this line

  DashPurchases(this.counter);

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
}

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

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

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

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

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

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

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

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

يتلقّى التطبيق الآن إشعارات بشأن عمليات الشراء، لذا ستجري عملية شراء في القسم التالي.

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

$ flutter test

00:01 +1: All tests passed!

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

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

Adapt PurchasableProduct

تعرض 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 = [];.

Load available purchases

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

lib/logic/dash_purchases.dart

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

عندما يكون المتجر متاحًا، حمِّل عمليات الشراء المتاحة. بما أنّك أجريت عملية الإعداد السابقة على Google Play وApp Store، من المتوقّع أن يظهر لك 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);
    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();                                       // Add this line
  }

أخيرًا، غيِّر قيمة الحقل 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);
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
      default:
        throw ArgumentError.value(
          product.productDetails,
          '${product.id} is not a known product',
        );
    }
  }

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

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();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

    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 Android Developer API في Google Cloud Console. 629f0bd8e6b50be8.png في حال طلب منك Google Play Console إنشاء مشروع أو الربط بمشروع حالي، عليك تنفيذ ذلك أولاً ثم الرجوع إلى هذه الصفحة.
  2. بعد ذلك، انتقِل إلى صفحة حسابات الخدمة وانقر على + إنشاء حساب خدمة. 8dc97e3b1262328a.png
  3. أدخِل اسم حساب الخدمة وانقر على إنشاء ومتابعة. 4fe8106af85ce75f.png
  4. اختَر دور مشترك Pub/Sub وانقر على تم. a5b6fa6ea8ee22d.png
  5. بعد إنشاء الحساب، انتقِل إلى إدارة المفاتيح. eb36da2c1ad6dd06.png
  6. انقر على إضافة مفتاح > إنشاء مفتاح جديد. e92db9557a28a479.png
  7. أنشئ مفتاح JSON وقم بتنزيله. 711d04f2f4176333.png
  8. أعِد تسمية الملف الذي تم تنزيله إلى service-account-google-play.json, وانقله إلى الدليل assets/.
  9. بعد ذلك، انتقِل إلى صفحة المستخدمون والأذونات في Play Console28fffbfc35b45f97.png
  10. انقر على دعوة مستخدمين جدد وأدخِل عنوان البريد الإلكتروني لحساب الخدمة الذي تم إنشاؤه سابقًا. يمكنك العثور على البريد الإلكتروني في الجدول ضمن صفحة حسابات الخدمةe3310cc077f397d.png
  11. امنح التطبيق الإذنَين عرض البيانات المالية وإدارة الطلبات والاشتراكات. a3b8cf2b660d1900.png
  12. انقر على دعوة مستخدم.

يجب أيضًا فتح lib/constants.dart, واستبدال قيمة androidPackageId بالمعرّف الذي اخترته لحزمة تطبيق Android.

إعداد إذن الوصول إلى Apple App Store

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

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

ملف إعداد الثوابت

قبل المتابعة، تأكَّد من ضبط الثوابت التالية في ملف 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 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

class PurchasePage extends StatelessWidget {
  const PurchasePage({super.key});

  @override
  Widget build(BuildContext context) {                     // Update from here
    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();
    }                                                      // To here.

    // ...

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

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

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

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

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart';                           // And this one

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

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

  final iapConnection = IAPConnection.instance;

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

أضِف firebaseNotifier عند إنشاء DashPurchases في main.dart:

lib/main.dart

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

أضِف دالة getter للمستخدم في FirebaseNotifier، حتى تتمكّن من تمرير معرّف المستخدم إلى دالة verify purchase.

lib/logic/firebase_notifier.dart

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

أضِف الدالة _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) {
      return true;
    } else {
      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();
          case storeKeyConsumable:
            counter.addBoughtDashes(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

    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,
  }) async {
    return true;
  }

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

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

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

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

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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.call);
}

({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 واستخرِج المعلومات التالية:
    1. userId: رقم تعريف المستخدم الذي سجّل الدخول
    2. source: المتجر المستخدَم، إما app_store أو google_play
    3. productData: تم الحصول عليه من productDataMap الذي أنشأته سابقًا.
    4. token: يحتوي على بيانات التحقّق التي سيتم إرسالها إلى المتاجر.
  3. استدعاء الطريقة verifyPurchase، إما للسمة GooglePlayPurchaseHandler أو AppStorePurchaseHandler، حسب المصدر
  4. في حال نجاح عملية التحقّق، تعرض الطريقة Response.ok للعميل.
  5. إذا فشلت عملية التحقّق، ستعرض الطريقة Response.internalServerError للعميل.

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

bin/server.dart

import 'dart:convert';
import 'dart:io'; // new

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @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 don't 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 don't 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

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,
  };
}

/// 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;
}

من المفترض أن يتم الآن التحقّق من عمليات الشراء التي أجريتها على 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 {

    // See next step
  }

الآن، نفِّذ 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 أحداث الفوترة من خلال ما يُعرف باسم موضوع Cloud Pub/Sub. وهي في الأساس قوائم انتظار للرسائل يمكن نشر الرسائل عليها، بالإضافة إلى استهلاكها منها.

بما أنّ هذه الوظيفة خاصة بـ 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 لاستدعاء الطريقة _pullMessageFromPubSub كل عشر ثوانٍ. يمكنك تعديل "المدة" حسب تفضيلاتك.

بعد ذلك، أنشئوا _pullMessageFromPubSub

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/$googleCloudProjectId/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/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

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

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

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

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

bin/server.dart

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

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

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

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

وأخيرًا، مرِّرها إلى الدالة الإنشائية GooglePlayPurchaseHandler:

bin/server.dart

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

إعداد Google Play

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

أولاً، أنشئ موضوعًا للنشر/الاشتراك:

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

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

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

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

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

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

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

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,                                  // And this parameter
  );

عدِّل الدالة الإنشائية لإضافة مؤقّت سيتم استخدامه لاستدعاء الطريقة _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

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  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 API:

bin/server.dart

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


  // 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,                                     // Add this argument
    ),
  };

إعداد App Store

بعد ذلك، عليك إعداد App Store باتّباع الخطوات التالية:

  1. سجِّل الدخول إلى App Store Connect، ثم اختَر المستخدمون وإمكانية الوصول.
  2. انتقِل إلى عمليات الدمج > المفاتيح > الشراء داخل التطبيق.
  3. انقر على رمز علامة الجمع لإضافة رمز جديد.
  4. أدخِل اسمًا، مثل "مفتاح Codelab".
  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((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

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

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

  Future<void> loadPurchases() async {
    // Elided.
  }

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);                 // Add this line
    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>(),                         // Add this line
          ),
          lazy: false,
        ),

بعد ذلك، اكتب الرمز البرمجي للدالة purchaseUpdate(). في dash_counter.dart,، تضبط الطريقتان applyPaidMultiplier وremovePaidMultiplier المضاعِف على 10 أو 1 على التوالي، لذا ليس عليك التحقّق مما إذا كان قد تم تطبيق الاشتراك من قبل. عندما تتغيّر حالة الاشتراك، عليك أيضًا تعديل حالة المنتج القابل للشراء حتى تتمكّن من عرض أنّه نشط حاليًا في صفحة الشراء. اضبط السمة _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_folder.png complete.

لمزيد من المعلومات، جرِّب دروس Flutter البرمجية الأخرى.