1. مقدمة
تتطلّب إضافة عمليات الشراء داخل التطبيق إلى تطبيق Flutter إعداد متجرَي App وPlay بشكل صحيح، والتحقّق من عملية الشراء، ومنح الأذونات اللازمة، مثل مزايا الاشتراك.
في هذا الدرس العملي، ستضيف ثلاثة أنواع من عمليات الشراء داخل التطبيق إلى تطبيق (مقدَّم لك)، وستتحقّق من عمليات الشراء هذه باستخدام خادم خلفي مكتوب بلغة Dart مع Firebase. يحتوي التطبيق المقدَّم، Dash Clicker، على لعبة تستخدم التميمة Dash كعملة. ستضيف خيارات الشراء التالية:
- خيار شراء متكرّر لـ 2000 Dash في المرة الواحدة
- شراء ترقية لمرة واحدة لتحويل لوحة البيانات القديمة إلى لوحة بيانات عصرية
- اشتراك يضاعف عدد النقرات التي يتم إنشاؤها تلقائيًا.
يمنح خيار الشراء الأول المستخدم فائدة مباشرة تبلغ 2,000 Dash. وهي متاحة للمستخدم مباشرةً ويمكن شراؤها عدة مرات. يُطلق على هذا المنتج اسم "منتج قابل للاستهلاك" لأنّه يُستهلك مباشرةً ويمكن استهلاكه عدة مرات.
يؤدي الخيار الثاني إلى ترقية Dash إلى إصدار أكثر جمالاً. يجب شراء هذا المنتج مرة واحدة فقط، وسيكون متاحًا إلى الأبد. ويُطلق على عملية الشراء هذه اسم "غير استهلاكية" لأنّ التطبيق لا يمكنه استهلاكها، ولكنها تظل صالحة إلى الأبد.
خيار الشراء الثالث والأخير هو الاشتراك. أثناء تفعيل الاشتراك، سيحصل المستخدم على المزيد من نقاط Dash بشكل أسرع، ولكن عندما يتوقف عن الدفع مقابل الاشتراك، ستتوقف المزايا أيضًا.
تعمل خدمة الخلفية (الموفّرة لك أيضًا) كتطبيق Dart، وتتحقّق من عمليات الشراء، وتخزِّنها باستخدام Firestore. يتم استخدام Firestore لتسهيل العملية، ولكن يمكنك استخدام أي نوع من خدمات الخلفية في تطبيقك المتاح للجميع.
ما ستنشئه
- ستوسّع نطاق تطبيقك ليشمل عمليات الشراء والاستهلاك والاشتراكات.
- ستوسّع أيضًا تطبيقًا للخدمة الخلفية بلغة Dart للتحقّق من المنتجات التي تم شراؤها وتخزينها.
ما ستتعرّف عليه
- كيفية ضبط App Store وPlay Store باستخدام المنتجات القابلة للشراء
- كيفية التواصل مع المتاجر للتحقّق من عمليات الشراء وتخزينها في Firestore
- كيفية إدارة عمليات الشراء داخل تطبيقك
المتطلبات
- استوديو Android
- Xcode (لتطوير تطبيقات iOS)
- حزمة تطوير البرامج (SDK) في Flutter
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.
في بنية المجلدات في Xcode، يقع مشروع Runner في الأعلى، وتقع أهداف Flutter وRunner وProducts تحت مشروع Runner. انقر مرّتين على المشغّل لتعديل إعدادات مشروعك، ثم انقر على التوقيع والقدرات. أدخِل معرّف الحزمة الذي اخترته للتو ضمن حقل الفريق لضبط فريقك.
يمكنك الآن إغلاق Xcode والعودة إلى Android Studio لإنهاء عملية الإعداد على Android. لإجراء ذلك، افتح ملف build.gradle.kts
ضمن android/app,
وغيِّر applicationId
(في السطر 24 في لقطة الشاشة أدناه) إلى معرّف التطبيق، وهو نفسه معرّف حِزمة iOS. يُرجى العِلم أنّه ليس من الضروري أن تكون أرقام التعريف في متجرَي iOS وAndroid متطابقة، ولكن من الأسهل تجنُّب الأخطاء عند تطابقها، لذا سنستخدم في هذا الدرس النموذجي معرّفات متطابقة.
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/، ثم انقر على الاتفاقيات والضرائب والشؤون المصرفية.
ستظهر لك هنا اتفاقيات التطبيقات المجانية والمدفوعة. يجب أن تكون حالة التطبيقات المجانية نشطة، وحالة التطبيقات المدفوعة هي "جديد". تأكَّد من الاطّلاع على البنود وقبولها وإدخال جميع المعلومات المطلوبة.
عند ضبط كل شيء بشكل صحيح، ستكون حالة التطبيقات المدفوعة نشطة. هذا أمر مهم للغاية لأنّك لن تتمكّن من تجربة عمليات الشراء داخل التطبيق بدون اتفاقية نشطة.
تسجيل معرّف التطبيق
أنشِئ معرّفًا جديدًا في Apple Developer Portal. انتقِل إلى developer.apple.com/account/resources/identifiers/list وانقر على رمز الإضافة بجانب العنوان المعرّفات.
اختيار معرّفات التطبيقات
اختيار تطبيق
قدِّم بعض الوصف واضبط معرّف الحزمة ليتطابق مع القيمة نفسها التي تم ضبطها سابقًا في XCode.
لمزيد من الإرشادات حول كيفية إنشاء معرّف تطبيق جديد، يُرجى الاطّلاع على مساعدة حساب المطوّر.
إنشاء تطبيق جديد
أنشئ تطبيقًا جديدًا في App Store Connect باستخدام معرّف الحزمة الفريد.
للحصول على مزيد من الإرشادات حول كيفية إنشاء تطبيق جديد وإدارة الاتفاقيات، يُرجى الاطّلاع على مساعدة App Store Connect.
لاختبار عمليات الشراء داخل التطبيق، تحتاج إلى مستخدم تجريبي في بيئة الاختبار المعزولة. يجب ألا يكون هذا المستخدم التجريبي مرتبطًا بـ iTunes، بل يُستخدم فقط لاختبار عمليات الشراء داخل التطبيق. لا يمكنك استخدام عنوان بريد إلكتروني سبق أن استخدمته لإنشاء حساب على Apple. في قسم المستخدمون وإمكانية الوصول، انتقِل إلى بيئة الاختبار المعزولة لإنشاء حساب جديد في بيئة الاختبار المعزولة أو لإدارة معرّفات Apple الحالية في بيئة الاختبار المعزولة.
يمكنك الآن إعداد مستخدم وضع الحماية على جهاز iPhone من خلال الانتقال إلى الإعدادات > المطوّر > حساب Apple في وضع الحماية.
ضبط عمليات الشراء داخل التطبيق
ستضبط الآن إعدادات المنتجات الثلاثة القابلة للشراء:
-
dash_consumable_2k
: عملية شراء استهلاكية يمكن إجراؤها عدة مرات، وتمنح المستخدم 2, 000 من عملة Dashes (العملة داخل التطبيق) لكل عملية شراء. -
dash_upgrade_3d
: عملية شراء "ترقية" غير قابلة للاستهلاك يمكن شراؤها مرة واحدة فقط، وتمنح المستخدم Dash مختلفًا من الناحية الشكلية للنقر عليه. dash_subscription_doubler
: اشتراك يمنح المستخدم ضعف عدد النقرات في كل نقرة طوال مدة الاشتراك
انتقِل إلى عمليات الشراء داخل التطبيق.
أنشئ عمليات الشراء داخل التطبيق باستخدام المعرّفات المحدّدة:
- اضبط
dash_consumable_2k
كـ منتج للاستهلاك. استخدِمdash_consumable_2k
كمعرّف المنتج. يُستخدم الاسم المرجعي في App Store Connect فقط، ما عليك سوى ضبطه علىdash consumable 2k
.إعداد مدى التوفّر يجب أن يكون المنتج متاحًا في بلد مستخدم البيئة التجريبية.
أضِف السعر واضبطه على
$1.99
أو ما يعادله بعملة أخرى.أضِف عمليات الترجمة إلى اللغات المحلية لعملية الشراء. أضِف عملية الشراء
Spring is in the air
مع استخدام2000 dashes fly out
كوصف.أضِف لقطة شاشة للمراجعة. لا يهم المحتوى إلا إذا تم إرسال المنتج للمراجعة، ولكنّه مطلوب لكي يكون المنتج في الحالة "جاهز للإرسال"، وهو أمر ضروري عندما يجلب التطبيق المنتجات من App Store.
- إعداد
dash_upgrade_3d
كمنتج غير قابل للاستهلاك استخدِمdash_upgrade_3d
كمعرّف المنتج. اضبط اسم المرجع علىdash upgrade 3d
. أضِف عملية الشراء3D Dash
مع استخدامBrings your dash back to the future
كوصف. اضبط السعر على$0.99
. اضبط مدى التوفّر وحمِّل لقطة شاشة المراجعة بالطريقة نفسها التي اتّبعتها مع المنتجdash_consumable_2k
. - إعداد
dash_subscription_doubler
كـ اشتراك يتجدّد تلقائيًا تختلف خطوات الاشتراك قليلاً. عليك أولاً إنشاء مجموعة اشتراكات. عندما تكون اشتراكات متعدّدة جزءًا من المجموعة نفسها، يمكن للمستخدم الاشتراك في أحدها فقط في الوقت نفسه، ولكن يمكنه الترقية أو الرجوع إلى إصدار أقدم من هذه الاشتراكات. ما عليك سوى الاتصال بهذه المجموعةsubscriptions
.أضِف ترجمة إلى لغات أخرى لمجموعة الاشتراكات.
بعد ذلك، عليك إنشاء الاشتراك. اضبط "اسم المرجع" على
dash subscription doubler
و"معرّف المنتج" علىdash_subscription_doubler
.بعد ذلك، اختَر مدة الاشتراك لمدة أسبوع واحد واللغات. أطلِق على هذا الاشتراك الاسم
Jet Engine
مع الوصفDoubles your clicks
. اضبط السعر على$0.49
. اضبط مدى التوفّر وحمِّل لقطة شاشة المراجعة بالطريقة نفسها التي اتّبعتها مع المنتجdash_consumable_2k
.
من المفترض أن تظهر لك المنتجات الآن في القوائم:
5- إعداد "متجر Play"
كما هو الحال مع App Store، ستحتاج أيضًا إلى حساب مطوّر على "متجر Play". إذا لم يكن لديك حساب، يمكنك تسجيل حساب.
إنشاء تطبيق جديد
أنشئ تطبيقًا جديدًا في Google Play Console باتّباع الخطوات التالية:
- افتح Play Console.
- انقر على جميع التطبيقات > إنشاء تطبيق.
- اختَر لغة تلقائية وأضِف عنوانًا لتطبيقك. اكتب اسم تطبيقك كما تريد أن يظهر على Google Play. يمكنك تغييره لاحقًا.
- حدِّد أنّ تطبيقك عبارة عن لعبة. يمكنك تغييرها لاحقًا.
- حدِّد ما إذا كان تطبيقك مجانيًا أو مدفوعًا.
- يجب الموافقة على "إرشادات المحتوى" و"قوانين التصدير الأمريكية".
- اختَر إنشاء تطبيق.
بعد إنشاء تطبيقك، انتقِل إلى لوحة البيانات وأكمِل جميع المهام في قسم إعداد تطبيقك. في هذه الصفحة، يمكنك تقديم بعض المعلومات عن تطبيقك، مثل التقييمات حسب الفئة العمرية ولقطات الشاشة.
توقيع التطبيق
لتتمكّن من اختبار عمليات الشراء داخل التطبيق، يجب تحميل إصدار واحد على الأقل إلى 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 في مكانَين:
- إلى مسار الاختبار المحدّد (الاختبار الداخلي)
- بصفتك مختبِر ترخيص
ابدأ أولاً بإضافة المختبِر إلى مسار الاختبار الداخلي. عُد إلى الاختبار والإصدار > الاختبار > الاختبار الداخلي وانقر على علامة التبويب المختبِرون.
أنشئ قائمة عناوين بريد إلكتروني جديدة من خلال النقر على إنشاء قائمة عناوين بريد إلكتروني. أدخِل اسمًا للقائمة، وأضِف عناوين البريد الإلكتروني لحسابات Google التي تحتاج إلى إذن الوصول إلى اختبار عمليات الشراء داخل التطبيق.
بعد ذلك، ضَع علامة في مربّع الاختيار بجانب القائمة، وانقر على حفظ التغييرات.
بعد ذلك، أضِف مختبِري الترخيص باتّباع الخطوات التالية:
- ارجع إلى عرض جميع التطبيقات في Google Play Console.
- انتقِل إلى الإعدادات > اختبار الترخيص.
- أضِف عناوين البريد الإلكتروني نفسها للمختبِرين الذين يحتاجون إلى اختبار عمليات الشراء داخل التطبيق.
- اضبط ردّ الترخيص على
RESPOND_NORMALLY
. - انقر على حفظ التغييرات.
ضبط عمليات الشراء داخل التطبيق
الآن، عليك ضبط المنتجات التي يمكن شراؤها داخل التطبيق.
كما هو الحال في App Store، عليك تحديد ثلاث عمليات شراء مختلفة:
-
dash_consumable_2k
: عملية شراء استهلاكية يمكن إجراؤها عدة مرات، وتمنح المستخدم 2, 000 من عملة Dashes (العملة داخل التطبيق) لكل عملية شراء. -
dash_upgrade_3d
: عملية شراء "ترقية" غير قابلة للاستهلاك يمكن شراؤها مرة واحدة فقط، وتمنح المستخدم Dash مختلفًا من الناحية الشكلية للنقر عليه. dash_subscription_doubler
: اشتراك يمنح المستخدم ضعف عدد النقرات في كل نقرة طوال مدة الاشتراك
أولاً، أضِف المنتجات الاستهلاكية وغير الاستهلاكية.
- انتقِل إلى Google Play Console واختَر تطبيقك.
- انتقِل إلى تحقيق الربح > المنتجات > المنتجات داخل التطبيق.
- انقر على إنشاء منتج
- أدخِل جميع المعلومات المطلوبة عن منتجك. تأكَّد من أنّ معرّف المنتج يتطابق تمامًا مع المعرّف الذي تريد استخدامه.
- انقر على حفظ.
- انقر على تفعيل.
- كرِّر العملية لشراء "الترقية" غير الاستهلاكية.
بعد ذلك، أضِف الاشتراك باتّباع الخطوات التالية:
- انتقِل إلى Google Play Console واختَر تطبيقك.
- انتقِل إلى تحقيق الربح > المنتجات > الاشتراكات.
- انقر على إنشاء اشتراك
- أدخِل جميع المعلومات المطلوبة لاشتراكك. تأكَّد من أنّ معرّف المنتج يتطابق تمامًا مع المعرّف الذي تريد استخدامه.
- انقر على حفظ.
من المفترض أن تكون عمليات الشراء قد تم إعدادها الآن في Play Console.
6. إعداد Firebase
في هذا الدرس التطبيقي حول الترميز، ستستخدم خدمة خلفية للتحقّق من عمليات الشراء التي يجريها المستخدمون وتتبُّعها.
لاستخدام خدمة الخلفية عدة مزايا:
- يمكنك إثبات ملكية الحساب بشكل آمن.
- يمكنك التفاعل مع أحداث الفوترة من متاجر التطبيقات.
- يمكنك تتبُّع عمليات الشراء في قاعدة بيانات.
- لن يتمكّن المستخدمون من خداع تطبيقك لتقديم ميزات مميّزة من خلال إعادة ضبط ساعة النظام.
على الرغم من توفّر العديد من الطرق لإعداد خدمة خلفية، ستستخدم وظائف السحابة الإلكترونية وFirestore، وذلك باستخدام Firebase من Google.
لا يتضمّن هذا الدرس التطبيقي العملي كتابة الخلفية، لذا يتضمّن رمز البداية مشروعًا على Firebase يتعامل مع عمليات الشراء الأساسية لمساعدتك في البدء.
يتم أيضًا تضمين مكوّنات Firebase الإضافية مع التطبيق التجريبي.
ما عليك سوى إنشاء مشروعك الخاص على Firebase، وإعداد كلّ من التطبيق والخادم الخلفي على Firebase، ثم نشر الخادم الخلفي.
إنشاء مشروع على Firebase
انتقِل إلى وحدة تحكُّم Firebase وأنشِئ مشروعًا جديدًا على Firebase. في هذا المثال، سمِّ المشروع Dash Clicker.
في تطبيق الخلفية، تربط عمليات الشراء بمستخدم معيّن، لذا تحتاج إلى مصادقة. لإجراء ذلك، استخدِم وحدة المصادقة في Firebase مع ميزة "تسجيل الدخول باستخدام حساب Google".
- من لوحة بيانات Firebase، انتقِل إلى المصادقة وفعِّلها إذا لزم الأمر.
- انتقِل إلى علامة التبويب طريقة تسجيل الدخول، وفعِّل موفِّر تسجيل الدخول Google.
بما أنّك ستستخدم أيضًا قاعدة بيانات Firestore من Firebase، عليك تفعيلها أيضًا.
اضبط قواعد 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).
للسماح بتسجيل الدخول باستخدام حساب 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
.
تمّت إضافة زوج المفتاح/القيمة من قبل، ولكن يجب استبدال قيمهما:
- احصل على قيمة
REVERSED_CLIENT_ID
من الملفGoogleService-Info.plist
، بدون العنصر<string>..</string>
المحيط بها. - استبدِل القيمة في ملف
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
يتضمّن صفحتَين. تنشئ هذه الصفحة أيضًا ثلاث Provider
s لكل من 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 إذا تم إعدادها بشكل صحيح. يُرجى العِلم أنّه قد يستغرق توفّر عمليات الشراء بعض الوقت عند إدخالها في وحدات التحكّم المعنية.
ارجع إلى 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/
كدليل جذري.
تأكَّد من تثبيت الأدوات التالية:
- Dart
- Firebase CLI
نظرة عامة على المشروع الأساسي
بما أنّ بعض أجزاء هذا المشروع تُعتبر خارج نطاق هذا الدرس البرمجي، تم تضمينها في رمز البداية. من المستحسن مراجعة المحتوى المتوفّر في الرمز الأولي قبل البدء، وذلك للحصول على فكرة عن كيفية تنظيم المحتوى.
يمكن تشغيل رمز الخلفية هذا محليًا على جهازك، ولا تحتاج إلى نشره لاستخدامه. ومع ذلك، يجب أن تتمكّن من الاتصال من جهاز التطوير (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 والانتقال إلى قسم حسابات الخدمة، ثم اختيار إنشاء مفتاح خاص جديد.
انسخ ملف JSON الذي تم تنزيله إلى المجلد assets/
، وأعِد تسميته إلى service-account-firebase.json
.
إعداد إذن الوصول إلى Google Play
للوصول إلى "متجر Play" من أجل التحقّق من عمليات الشراء، عليك إنشاء حساب خدمة بهذه الأذونات وتنزيل بيانات اعتماد JSON الخاصة به.
- انتقِل إلى صفحة Google Play Android Developer API في Google Cloud Console.
في حال طلب منك Google Play Console إنشاء مشروع أو الربط بمشروع حالي، عليك تنفيذ ذلك أولاً ثم الرجوع إلى هذه الصفحة.
- بعد ذلك، انتقِل إلى صفحة حسابات الخدمة وانقر على + إنشاء حساب خدمة.
- أدخِل اسم حساب الخدمة وانقر على إنشاء ومتابعة.
- اختَر دور مشترك Pub/Sub وانقر على تم.
- بعد إنشاء الحساب، انتقِل إلى إدارة المفاتيح.
- انقر على إضافة مفتاح > إنشاء مفتاح جديد.
- أنشئ مفتاح JSON وقم بتنزيله.
- أعِد تسمية الملف الذي تم تنزيله إلى
service-account-google-play.json,
وانقله إلى الدليلassets/
. - بعد ذلك، انتقِل إلى صفحة المستخدمون والأذونات في Play Console
- انقر على دعوة مستخدمين جدد وأدخِل عنوان البريد الإلكتروني لحساب الخدمة الذي تم إنشاؤه سابقًا. يمكنك العثور على البريد الإلكتروني في الجدول ضمن صفحة حسابات الخدمة
- امنح التطبيق الإذنَين عرض البيانات المالية وإدارة الطلبات والاشتراكات.
- انقر على دعوة مستخدم.
يجب أيضًا فتح lib/constants.dart,
واستبدال قيمة androidPackageId
بالمعرّف الذي اخترته لحزمة تطبيق Android.
إعداد إذن الوصول إلى Apple App Store
للوصول إلى App Store من أجل التحقّق من عمليات الشراء، عليك إعداد سر مشترك باتّباع الخطوات التالية:
- افتح App Store Connect.
- انتقِل إلى تطبيقاتي واختَر تطبيقك.
- في شريط التنقّل الجانبي، انتقِل إلى الإعدادات العامة > معلومات التطبيق.
- انقر على إدارة ضمن عنوان السر المشترك الخاص بالتطبيق.
- أنشئ مفتاحًا سرّيًا جديدًا وانسَخه.
- افتح
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
مع عمليات تنفيذ منفصلة لكل متجر.
ابدأ بإضافة ملف 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');
}
}
تنفّذ التعليمة البرمجية ما يلي:
- حدِّد نقطة نهاية POST سيتم استدعاؤها من التطبيق الذي أنشأته سابقًا.
- فك ترميز حِمل JSON واستخرِج المعلومات التالية:
-
userId
: رقم تعريف المستخدم الذي سجّل الدخول -
source
: المتجر المستخدَم، إماapp_store
أوgoogle_play
productData
: تم الحصول عليه منproductDataMap
الذي أنشأته سابقًا.-
token
: يحتوي على بيانات التحقّق التي سيتم إرسالها إلى المتاجر.
-
- استدعاء الطريقة
verifyPurchase
، إما للسمةGooglePlayPurchaseHandler
أوAppStorePurchaseHandler
، حسب المصدر - في حال نجاح عملية التحقّق، تعرض الطريقة
Response.ok
للعميل. - إذا فشلت عملية التحقّق، ستعرض الطريقة
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
لقد كتبت الرمز البرمجي لاستهلاك أحداث الفوترة من موضوع النشر/الاشتراك، ولكنّك لم تنشئ موضوع النشر/الاشتراك، كما أنّك لا تنشر أي أحداث فوترة. حان وقت إعداد هذه الميزة.
أولاً، أنشئ موضوعًا للنشر/الاشتراك:
- اضبط قيمة
googleCloudProjectId
فيconstants.dart
على معرّف مشروعك على Google Cloud. - انتقِل إلى صفحة Cloud Pub/Sub في Google Cloud Console.
- تأكَّد من أنّك في مشروعك على Firebase، ثم انقر على + إنشاء موضوع.
- امنح الموضوع الجديد اسمًا مطابقًا للقيمة التي تم ضبطها لـ
googlePlayPubsubBillingTopic
فيconstants.dart
. في هذه الحالة، سمِّهplay_billing
. إذا اخترت شيئًا آخر، احرص على تعديلconstants.dart
. أنشئ الموضوع. - في قائمة مواضيع النشر/الاشتراك، انقر على النقاط الثلاث العمودية للموضوع الذي أنشأته للتو، ثم انقر على عرض الأذونات.
- في الشريط الجانبي على يسار الشاشة، اختَر إضافة مسؤول.
- أضِف هنا
google-play-developer-notifications@system.gserviceaccount.com
، وامنحه دور ناشر Pub/Sub. - احفظ التغييرات التي أجريتها على الأذونات.
- انسخ اسم الموضوع الذي أنشأته للتو.
- افتح Play Console مرة أخرى، واختَر تطبيقك من قائمة كل التطبيقات.
- انتقِل للأسفل إلى تحقيق الربح > إعداد تحقيق الربح.
- املأ الموضوع الكامل واحفظ التغييرات.
سيتم الآن نشر جميع أحداث الفوترة في 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,
),
);
}
}
}
}
تعمل هذه الطريقة على النحو التالي:
- يحصل على قائمة الاشتراكات النشطة من Firestore باستخدام IapRepository.
- بالنسبة إلى كل طلب، يتم طلب حالة الاشتراك من App Store Server API.
- يحصل على آخر معاملة لعملية شراء الاشتراك هذه.
- التحقّق من تاريخ انتهاء الصلاحية
- تعدّل هذه السمة حالة الاشتراك على 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 باتّباع الخطوات التالية:
- سجِّل الدخول إلى App Store Connect، ثم اختَر المستخدمون وإمكانية الوصول.
- انتقِل إلى عمليات الدمج > المفاتيح > الشراء داخل التطبيق.
- انقر على رمز علامة الجمع لإضافة رمز جديد.
- أدخِل اسمًا، مثل "مفتاح Codelab".
- نزِّل ملف p8 الذي يحتوي على المفتاح.
- انسخها إلى مجلد مواد العرض بالاسم
SubscriptionKey.p8
. - انسخ رقم تعريف المفتاح من المفتاح الذي تم إنشاؤه حديثًا واضبطه على الثابت
appStoreKeyId
في الملفlib/constants.dart
. - انسخ معرّف جهة الإصدار في أعلى قائمة المفاتيح مباشرةً، واضبطه على الثابت
appStoreIssuerId
في الملفlib/constants.dart
.
تتبُّع عمليات الشراء على الجهاز
إنّ الطريقة الأكثر أمانًا لتتبُّع عمليات الشراء هي من جهة الخادم لأنّه يصعب تأمين البرنامج، ولكن يجب أن تتوفّر طريقة لإعادة المعلومات إلى البرنامج حتى يتمكّن التطبيق من اتّخاذ إجراء بشأن معلومات حالة الاشتراك. من خلال تخزين عمليات الشراء في 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. أكملت الخطوات بنجاح
تهانينا!!! لقد أكملت تجربة البرمجة. يمكنك العثور على الرمز البرمجي المكتمل لهذا الدرس العملي في المجلد complete.
لمزيد من المعلومات، جرِّب دروس Flutter البرمجية الأخرى.