1. مقدمة
تتطلّب إضافة ميزة الشراء داخل التطبيق إلى تطبيق Flutter إعداد "متجر Play" و"متجر التطبيقات" بشكل صحيح والتحقّق من عملية الشراء ومنح الأذونات اللازمة، مثل مزايا الاشتراك.
في هذا الدليل التعليمي حول الرموز البرمجية، ستضيف ثلاثة أنواع من عمليات الشراء داخل التطبيق إلى تطبيق (مقدَّم لك)، وستتحقّق من هذه عمليات الشراء باستخدام خلفية Dart مع Firebase. يحتوي التطبيق المقدَّم، Dash Clicker، على لعبة تستخدم التميمة Dash كعملة. ستضيف خيارات الشراء التالية:
- خيار شراء متكرّر لعدد 2000 رمز Dash في المرة الواحدة
- شراء ترقية لمرة واحدة لتحويل Dash القديمة إلى Dash ذات نمط حديث
- اشتراك يضاعف عدد النقرات التي يتم إنشاؤها تلقائيًا
يمنحك خيار الشراء الأول ميزة مباشرة تتمثل في 2000 داش. وتتوفّر هذه العناصر للمستخدم مباشرةً ويمكن شراؤها عدة مرات. ويُعرف هذا المحتوى باسم المحتوى القابل للاستهلاك لأنّه يتم استهلاكه مباشرةً ويمكن استهلاكه عدة مرات.
يؤدي الخيار الثاني إلى ترقية Dash إلى Dash أكثر جمالًا. يجب شراء هذا الاشتراك مرة واحدة فقط، وهو متاح إلى الأبد. وتُعرف هذه العملية باسم "الشراء غير الاستهلاكي" لأنّه لا يمكن للتطبيق استخدامها، ولكنها صالحة إلى الأبد.
الخيار الثالث والأخير للشراء هو الاشتراك. عندما يكون الاشتراك نشطًا، سيحصل المستخدم على Dashes بشكل أسرع، ولكن عندما يتوقف عن الدفع مقابل الاشتراك، ستتوقف المزايا أيضًا.
تعمل خدمة الخلفية (التي يتم توفيرها لك أيضًا) كتطبيق Dart، وتتحقّق من إجراء عمليات الشراء وتخزّنها باستخدام Firestore. يتم استخدام Firestore لتسهيل العملية، ولكن في تطبيقك العلني، يمكنك استخدام أي نوع من خدمات الخلفية.
ما ستُنشئه
- ستوفّر اشتراكات وعمليات شراء للمحتوى القابل للاستهلاك في تطبيقك.
- ستحتاج أيضًا إلى إضافة تطبيق خلفية مكتوب بلغة Dart لإثبات صحة العناصر التي تم شراؤها وتخزينها.
المعلومات التي ستتعرّف عليها
- كيفية ضبط "متجر التطبيقات" و"متجر Play" للسماح بشراء المنتجات
- كيفية التواصل مع المتاجر للتحقّق من عمليات الشراء وتخزينها في Firestore
- كيفية إدارة عمليات الشراء في تطبيقك
المتطلبات
- الإصدار 4.1 من "استوديو Android" أو إصدار أحدث
- Xcode 12 أو إصدار أحدث (لتطوير التطبيقات على نظام التشغيل iOS)
- حزمة تطوير البرامج (SDK) من Flutter
2. إعداد بيئة التطوير
لبدء هذا الدليل التعليمي حول الرموز البرمجية، عليك تنزيل الرمز البرمجي وتغيير معرّف الحزمة لنظام التشغيل iOS واسم الحزمة لنظام التشغيل Android.
تنزيل الرمز
لاستنساخ مستودع GitHub من سطر الأوامر، استخدِم الأمر التالي:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
أو إذا كانت أداة cli من GitHub مثبَّتة، يمكنك استخدام الأمر التالي:
gh repo clone flutter/codelabs flutter-codelabs
يتم استنساخ نموذج الرمز البرمجي في دليل flutter-codelabs
يحتوي على الرمز البرمجي لمجموعة من ورشات عمل رموز برمجية. رمز هذا الدرس التطبيقي حول الترميز باللغة flutter-codelabs/in_app_purchases
.
يحتوي بنية الدليل ضمن flutter-codelabs/in_app_purchases
على سلسلة من اللقطات التي توضّح النقطة التي يجب أن تكون فيها في نهاية كل خطوة مُسمّاة. يتوفّر الرمز المبدئي في الخطوة 0، لذا يمكنك العثور على الملفات المطابقة بسهولة من خلال اتّباع الخطوات التالية:
cd flutter-codelabs/in_app_purchases/step_00
إذا كنت تريد التقديم أو معرفة الشكل الذي يجب أن يظهر به العنصر بعد خطوة معيّنة، ابحث في الدليل الذي يحمل اسم الخطوة التي تهمّك. يمكن العثور على رمز الخطوة الأخيرة ضمن المجلد complete
.
إعداد المشروع الأوّلي
افتح المشروع الأوّلي من step_00
في بيئة تطوير البرامج المتكاملة المفضّلة لديك. لقد استخدمنا "استوديو Android" لالتقاط لقطات الشاشة، ولكن Visual Studio Code هو خيار رائع أيضًا. باستخدام أي من المحرّرين، تأكَّد من تثبيت أحدث الإضافات لكل من Dart وFlutter.
يجب أن تتواصل التطبيقات التي ستنشئها مع App Store و"متجر Play" لمعرفة المنتجات المتوفّرة وسعرها. يتم التعرّف على كل تطبيق من خلال رقم تعريف فريد. يُعرف هذا المعرّف في "متجر التطبيقات" لنظام التشغيل iOS باسم "معرّف الحزمة"، وفي "متجر Play" لنظام التشغيل Android باسم "معرّف التطبيق". يتم إنشاء هذه المعرّفات عادةً باستخدام رمز اسم النطاق العكسي. على سبيل المثال، عند إجراء عملية شراء داخل تطبيق على flutter.dev، يمكنك استخدام dev.flutter.inapppurchase
. حدِّد معرّفًا لتطبيقك، وسيكون عليك ضبطه الآن في إعدادات المشروع.
أولاً، عليك إعداد معرّف الحزمة لنظام التشغيل iOS.
مع فتح المشروع في Android Studio، انقر بزر الماوس الأيمن على مجلد iOS، ثم انقر على Flutter، وافتح الوحدة في تطبيق Xcode.
في بنية مجلد Xcode، يظهر مشروع Runner في أعلى الصفحة، وتظهر استهدافات Flutter وRunner وProducts أسفل مشروع Runner. انقر مرّتين على Runner لتعديل إعدادات مشروعك، ثم انقر على Signing & Capabilities (التوقيع والإمكانات). أدخِل معرّف الحِزمة الذي اخترته للتو ضمن حقل الفريق لضبط فريقك.
يمكنك الآن إغلاق Xcode والعودة إلى Android Studio لإكمال عملية الضبط لنظام التشغيل Android. لإجراء ذلك، افتح ملف build.gradle
ضمن android/app,
وغيِّر applicationId
(في السطر 37 من لقطة الشاشة أدناه) إلى معرّف التطبيق، وهو نفسه معرّف حِزمة iOS. تجدر الإشارة إلى أنّه ليس من الضروري أن تكون أرقام تعريف متجرَي iOS وAndroid متطابقة، ولكن يُفضّل إبقاؤها متطابقة لتجنّب حدوث أخطاء، ولذلك سنستخدم في هذا الدليل التعليمي أيضًا معرّفات متطابقة.
3- تثبيت المكوّن الإضافي
في هذا الجزء من ورشة رموز البرامج، ستثبّت المكوّن الإضافي in_app_purchase.
إضافة التبعية في pubspec
أضِف in_app_purchase
إلى pubspec عن طريق إضافة in_app_purchase
إلى التبعيات في pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
افتح pubspec.yaml
وتأكَّد من أنّ in_app_purchase
مُدرَج الآن كإدخال ضمن dependencies
وin_app_purchase_platform_interface
ضمن dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
انقر على pub get لتنزيل الحزمة أو نفِّذ flutter pub get
في سطر الأوامر.
4. إعداد App Store
لإعداد عمليات الشراء داخل التطبيق واختبارها على أجهزة iOS، عليك إنشاء تطبيق جديد في App Store وإنشاء منتجات قابلة للشراء فيه. ولن تحتاج إلى نشر أي محتوى أو إرسال التطبيق إلى Apple لمراجعته. يجب أن يكون لديك حساب مطوِّر لإجراء ذلك. إذا لم يكن لديك حساب، سجِّل في برنامج المطوّرين في Apple.
اتّفاقيات التطبيقات المدفوعة
لاستخدام ميزة "عمليات الشراء داخل التطبيق"، يجب أيضًا أن تكون لديك اتفاقية نشطة للتطبيقات المدفوعة في App Store Connect. انتقِل إلى https://appstoreconnect.apple.com/، ثم انقر على الأحكام والضرائب والمعاملات المصرفية.
ستظهر لك هنا اتفاقيات التطبيقات المجانية والمدفوعة. يجب أن تكون حالة التطبيقات المجانية نشطة، وحالة التطبيقات المدفوعة جديدة. يُرجى الاطّلاع على البنود وقبولها وإدخال جميع المعلومات المطلوبة.
عندما يتم ضبط كل الإعدادات بشكل صحيح، ستصبح حالة التطبيقات المدفوعة نشطة. هذا أمر مهم جدًا لأنّه لن تتمكّن من تجربة عمليات الشراء داخل التطبيق بدون اتفاقية نشطة.
تسجيل معرّف التطبيق
أنشئ معرّفًا جديدًا في مدخل المطوّرين في Apple.
اختيار معرّفات التطبيقات
اختيار تطبيق
قدِّم بعض الوصف واضبط معرّف الحزمة ليطابق القيمة نفسها التي تم ضبطها سابقًا في XCode.
لمزيد من الإرشادات حول كيفية إنشاء معرّف تطبيق جديد، يُرجى الاطّلاع على مساعدة حساب المطوّر .
إنشاء تطبيق جديد
أنشئ تطبيقًا جديدًا في App Store Connect باستخدام معرّف الحزمة الفريد.
لمزيد من الإرشادات حول كيفية إنشاء تطبيق جديد وإدارة الاتفاقيات، يُرجى الاطّلاع على مساعدة App Store Connect.
لاختبار عمليات الشراء داخل التطبيق، تحتاج إلى مستخدم اختبار في بيئة المحاكاة. يجب ألا يكون هذا المستخدم التجريبي مرتبطًا بحساب iTunes، إذ لا يتم استخدامه إلا لاختبار عمليات الشراء داخل التطبيق. لا يمكنك استخدام عنوان بريد إلكتروني سبق أن تم استخدامه لإنشاء حساب على Apple. في قسم المستخدمون وإمكانية الوصول، انتقِل إلى المختبِرون ضمن مساحة الاختبار لإنشاء حساب مساحة اختبار جديد أو لإدارة معرّفات Apple الحالية لمساحة الاختبار.
يمكنك الآن إعداد حساب وضع الحماية على هاتف iPhone من خلال الانتقال إلى الإعدادات > متجر التطبيقات > حساب وضع الحماية.
ضبط عمليات الشراء داخل التطبيق
عليك الآن ضبط العناصر الثلاثة القابلة للشراء:
dash_consumable_2k
: عملية شراء استهلاكية يمكن شراؤها عدة مرات، وتمنح المستخدم 2000 داش (العملة داخل التطبيق) لكل عملية شراء.dash_upgrade_3d
: عملية شراء "ترقية" غير قابلة للاستهلاك يمكن إجراؤها مرة واحدة فقط، وتمنح المستخدم رمز Dash مختلفًا من حيث الشكل للنقر عليه.dash_subscription_doubler
: اشتراك يمنح المستخدم عددًا أكبر من النقاط في كل نقرة بمقدار الضعف طوال مدة الاشتراك
انتقِل إلى عمليات الشراء داخل التطبيق > إدارة.
أنشئ عمليات الشراء داخل التطبيق باستخدام المعرّفات المحدّدة:
- اضبط
dash_consumable_2k
على أنّه سلعة استهلاكية.
استخدِم dash_consumable_2k
كمعرّف المنتج. لا يُستخدَم الاسم المرجعي إلا في App Store Connect، ما عليك سوى ضبطه على dash consumable 2k
وإضافة ترجمات عملية الشراء. اضبط عملية الشراء على Spring is in the air
مع 2000 dashes fly out
كوصف.
- اضبط
dash_upgrade_3d
على أنّه غير قابل للاستهلاك.
استخدِم dash_upgrade_3d
كمعرّف المنتج. اضبط الاسم المرجعي على dash upgrade 3d
وأضِف الترجمات للإجراء. اضبط عملية الشراء على 3D Dash
مع Brings your dash back to the future
كوصف.
- اضبط
dash_subscription_doubler
على أنّه اشتراك يتجدّد تلقائيًا.
تختلف عملية الاشتراكات قليلاً. عليك أولاً ضبط الاسم المرجعي ومعرّف المنتج:
بعد ذلك، عليك إنشاء مجموعة اشتراكات. عندما تكون هناك اشتراكات متعددة ضمن المجموعة نفسها، يمكن للمستخدم الاشتراك في اشتراك واحد منها فقط في الوقت نفسه، ولكن يمكنه الترقية أو الرجوع إلى خطة أقل كلفة بين هذه الاشتراكات بسهولة. ما عليك سوى تسمية هذه المجموعة subscriptions
.
بعد ذلك، أدخِل مدة الاشتراك وترجمات الاشتراك. أدخِل اسم "Jet Engine
" لهذا الاشتراك مع الوصف "Doubles your clicks
". انقر على حفظ.
بعد النقر على الزر حفظ، أضِف سعر اشتراك. اختَر أي سعر تريده.
من المفترض أن تظهر لك الآن عمليات الشراء الثلاث في قائمة عمليات الشراء:
5- إعداد "متجر Play"
كما هو الحال مع App Store، ستحتاج أيضًا إلى حساب مطوّر على "متجر Play". إذا لم يكن لديك حساب، يمكنك تسجيل حساب.
إنشاء تطبيق جديد
أنشئ تطبيقًا جديدًا في Google Play Console باتّباع الخطوات التالية:
- افتح Play Console.
- اختَر جميع التطبيقات > إنشاء تطبيق.
- اختَر لغة تلقائية وأضِف عنوانًا لتطبيقك. اكتب اسم تطبيقك كما تريد أن يظهر على Google Play. يمكنك تغييره لاحقًا.
- حدِّد أنّ تطبيقك لعبة. يمكنك تغييرها لاحقًا.
- حدِّد ما إذا كان تطبيقك مجانيًا أو مدفوعًا.
- أضِف عنوان بريد إلكتروني يمكن لمستخدمي "متجر 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
.
أضِف معلومات ملف تخزين المفاتيح من ملف الخصائص قبل القسم android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
حمِّل ملف key.properties
في عنصر keystoreProperties
.
أضِف الرمز التالي قبل مجموعة buildTypes
:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
اضبط القسم signingConfigs
في ملف build.gradle
الخاص بالوحدة باستخدام معلومات إعداد التوقيع:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
سيتم الآن توقيع إصدارات تطبيقك العلنية تلقائيًا.
لمزيد من المعلومات عن توقيع تطبيقك، يُرجى الاطّلاع على مقالة توقيع تطبيقك على developer.android.com.
تحميل الإصدار الأول
بعد ضبط تطبيقك للتوقيع، من المفترض أن تتمكّن من إنشاء تطبيقك من خلال تشغيل:
flutter build appbundle
ينشئ هذا الأمر إصدارًا علنيًا تلقائيًا ويمكن العثور على الإخراج على <your app dir>/build/app/outputs/bundle/release/
.
من لوحة البيانات في Google Play Console، انتقِل إلى الإصدار > الاختبار > الاختبار المغلق، وأنشئ إصدارًا جديدًا للاختبار المغلق.
في هذا الدليل التعليمي حول رموز البرامج، ستلتزم بتوقيع Google على التطبيق، لذا يُرجى المتابعة والضغط على متابعة ضمن ميزة "توقيع التطبيق" من Play لتفعيل هذه الميزة.
بعد ذلك، حمِّل حِزمة تطبيق app-release.aab
التي تم إنشاؤها باستخدام الأمر build.
انقر على حفظ، ثم على مراجعة الإصدار.
أخيرًا، انقر على بدء الطرح في الاختبار الداخلي لتفعيل إصدار الاختبار الداخلي.
إعداد مستخدمين تجريبيين
لكي تتمكّن من اختبار عمليات الشراء داخل التطبيق، يجب إضافة حسابات Google للمختبِرين في Google Play Console في موضعين:
- إلى مسار الاختبار المحدّد (الاختبار الداخلي)
- بصفتك مختبِر ترخيص
أولاً، ابدأ بإضافة المختبِر إلى مسار الاختبار الداخلي. ارجع إلى الإصدار > الاختبار > الاختبار الداخلي وانقر على علامة التبويب المختبِرون.
أنشئ قائمة عناوين بريد إلكتروني جديدة بالنقر على إنشاء قائمة عناوين بريد إلكتروني. أدخِل اسمًا للقائمة وأضِف عناوين البريد الإلكتروني لحسابات Google التي تحتاج إلى إذن الوصول لاختبار عمليات الشراء داخل التطبيق.
بعد ذلك، ضَع علامة في مربّع الاختيار الخاص بالقائمة، ثم انقر على حفظ التغييرات.
بعد ذلك، أضِف مختبِري الترخيص:
- ارجع إلى عرض كل التطبيقات في Google Play Console.
- انتقِل إلى الإعدادات > اختبار الترخيص.
- أضِف عناوين البريد الإلكتروني نفسها للمختبِرين الذين يحتاجون إلى اختبار عمليات الشراء داخل التطبيق.
- اضبط License response (ردّ الترخيص) على
RESPOND_NORMALLY
. - انقر على حفظ التغييرات.
ضبط عمليات الشراء داخل التطبيق
عليك الآن ضبط المنتجات القابلة للشراء داخل التطبيق.
تمامًا كما هو الحال في App Store، عليك تحديد ثلاث عمليات شراء مختلفة:
dash_consumable_2k
: عملية شراء استهلاكية يمكن شراؤها عدة مرات، وتمنح المستخدم 2000 داش (العملة داخل التطبيق) لكل عملية شراء.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 CLI. اتّبِع التعليمات الموضّحة في صفحة الإعداد.
عند تشغيل 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 واملأ الحقل الأخير في مربّع الحوار المنبثق لإرسال التطبيق.
إعداد Firebase لنظام التشغيل iOS: خطوات إضافية
افتح ios/Runnder.xcworkspace
باستخدام Xcode
. أو باستخدام بيئة تطوير البرامج المفضّلة لديك.
في VSCode، انقر بزر الماوس الأيمن على مجلد ios/
ثم على open in xcode
.
في Android Studio، انقر بزر الماوس الأيمن على مجلد ios/
ثم انقر على flutter
ثم على الخيار open iOS module in Xcode
.
للسماح بتسجيل الدخول باستخدام حساب Google على أجهزة iOS، أضِف خيار إعداد CFBundleURLTypes
إلى ملفات plist
الإصدار. (اطّلِع على مستندات حزمة google_sign_in
للحصول على مزيد من المعلومات). في هذه الحالة، الملفان هما ios/Runner/Info-Debug.plist
وios/Runner/Info-Release.plist
.
سبق أن تمّت إضافة زوج المفتاح/القيمة، ولكن يجب استبدال قيمته:
- الحصول على قيمة
REVERSED_CLIENT_ID
من ملفGoogleService-Info.plist
بدون العنصر<string>..</string>
المحيط به - استبدِل القيمة في كلٍّ من الملفَّين
ios/Runner/Info-Debug.plist
وios/Runner/Info-Release.plist
ضمن المفتاحCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
لقد انتهيت الآن من إعداد Firebase.
7- الاستماع إلى آخر المعلومات حول عمليات الشراء
في هذا الجزء من ورشة رموز البرامج، ستُعدّ التطبيق لشراء المنتجات. وتشمل هذه العملية الاستماع إلى آخر المعلومات حول عمليات الشراء والأخطاء بعد بدء تشغيل التطبيق.
الاستماع إلى آخر المعلومات حول عمليات الشراء
في main.dart,
، ابحث عن التطبيق المصغّر MyHomePage
الذي يحتوي على Scaffold
مع BottomNavigationBar
يحتوي على صفحتَين. تنشئ هذه الصفحة أيضًا ثلاث مجموعات Provider
لكل من DashCounter
وDashUpgrades,
وDashPurchases
. تتتبّع DashCounter
العدد الحالي للشرطة المائلة وتزيده تلقائيًا. تدير DashUpgrades
الترقيات التي يمكنك شراؤها باستخدام الرموز الرقمية Dashes. يركز هذا الدرس التطبيقي على DashPurchases
.
يتم تلقائيًا تحديد عنصر مقدّم الخدمة عند طلب هذا العنصر لأول مرة. يستمع هذا العنصر إلى تعديلات الشراء مباشرةً عند بدء تشغيل التطبيق، لذا أوقِف التحميل البطيء لهذا العنصر باستخدام lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // 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
، انتقِل إلى رمز DashPurchases ChangeNotifier
. في الوقت الحالي، لا يتوفّر سوى 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
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. إجراء عمليات شراء
في هذا الجزء من ورشة رموز البرامج، ستستبدِل المنتجات النموذجية الحالية بمنتجات حقيقية قابلة للشراء. يتم تحميل هذه المنتجات من المتاجر، ويتم عرضها في قائمة، ويمكن شراؤها عند النقر على المنتج.
تكييف 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 = [];
تحميل عمليات الشراء المتاحة
لمنح المستخدم إمكانية إجراء عملية شراء، حمِّل عمليات الشراء من المتجر. عليك أولاً التحقّق ممّا إذا كان المتجر متاحًا. عندما لا يكون المتجر متاحًا، يؤدي ضبط storeState
على notAvailable
إلى عرض رسالة خطأ للمستخدم.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
عندما يكون المتجر متاحًا، حمِّل عمليات الشراء المتاحة. استنادًا إلى الإعداد السابق في Firebase، من المتوقّع ظهور storeKeyConsumable
وstoreKeySubscription,
وstoreKeyUpgrade
. عندما لا تتوفّر عملية شراء متوقّعة، يمكنك طباعة هذه المعلومات في وحدة التحكّم، ويمكنك أيضًا إرسال هذه المعلومات إلى خدمة الخلفية.
تعرض الطريقة await iapConnection.queryProductDetails(ids)
كلّ من المعرّفات التي لم يتم العثور عليها والمنتجات القابلة للشراء التي تم العثور عليها. استخدِم productDetails
من الردّ لتعديل واجهة المستخدم، واضبط StoreState
على available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
استدعاء الدالة loadPurchases()
في الدالة الانشائية:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
أخيرًا، غيِّر قيمة الحقل storeState
من StoreState.available
إلى StoreState.loading:
.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
عرض المنتجات القابلة للشراء
فكِّر في ملف purchase_page.dart
. تعرِض أداة PurchasePage
_PurchasesLoading
أو _PurchaseList,
أو _PurchasesNotAvailable,
حسب StoreState
. تعرِض الأداة أيضًا عمليات الشراء السابقة للمستخدِم التي يتم استخدامها في الخطوة التالية.
تعرِض الأداة المصغّرة _PurchaseList
قائمة بالمنتجات القابلة للشراء وتُرسِل طلب شراء إلى عنصر DashPurchases
.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
من المفترض أن تتمكّن من رؤية المنتجات المتوفّرة في متاجر Android وiOS إذا تم ضبطها بشكل صحيح. يُرجى العِلم أنّه قد يستغرق الأمر بعض الوقت قبل توفّر عمليات الشراء عند إدخالها في وحدات التحكّم ذات الصلة.
ارجع إلى 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
للإشارة إليه.
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 والانتقال إلى قسم حسابات الخدمة، ثم اختيار إنشاء مفتاح خاص جديد.
انسخ ملف JSON الذي تم تنزيله إلى المجلد assets/
وأعِد تسميته باسم service-account-firebase.json
.
إعداد إمكانية الوصول إلى Google Play
للوصول إلى "متجر Play" لإثبات ملكية عمليات الشراء، عليك إنشاء حساب خدمة يتضمّن هذه الأذونات وتنزيل بيانات اعتماد JSON له.
- انتقِل إلى Google Play Console وابدأ من صفحة كل التطبيقات.
- انتقِل إلى الإعداد > الوصول إلى واجهة برمجة التطبيقات.
إذا طلبت منك Google Play Console إنشاء مشروع حالي أو ربطه، عليك إجراء ذلك أولاً ثم الرجوع إلى هذه الصفحة.
- ابحث عن القسم الذي يمكنك فيه تحديد حسابات الخدمة، وانقر على إنشاء حساب خدمة جديد.
- انقر على رابط Google Cloud Platform في مربّع الحوار المنبثق.
- اختَر مشروعك. إذا لم يظهر لك هذا الخيار، تأكَّد من تسجيل الدخول إلى حساب Google الصحيح ضمن القائمة المنسدلة الحساب في أعلى يسار الصفحة.
- بعد اختيار مشروعك، انقر على + إنشاء حساب خدمة في شريط القوائم في أعلى الصفحة.
- أدخِل اسمًا لحساب الخدمة، ويمكنك اختياريًا تقديم وصف حتى تتذكر الغرض منه، ثم انتقِل إلى الخطوة التالية.
- امنح حساب الخدمة دور المحرِّر.
- أكمِل معالج الإعدادات، ثم ارجع إلى صفحة الوصول إلى واجهة برمجة التطبيقات في "وحدة تحكّم المطوّر"، وانقر على إعادة تحميل حسابات الخدمات. من المفترض أن يظهر حسابك الذي أنشأته حديثًا في القائمة.
- انقر على منح الإذن بالوصول لحساب الخدمة الجديد.
- انتقِل إلى أسفل الصفحة التالية، إلى مجموعة البيانات المالية. اختَر كلّ من عرض البيانات المالية والطلبات والردود على استطلاعات أسباب الإلغاء وإدارة الطلبات والاشتراكات.
- انقر على دعوة مستخدم.
- بعد إعداد الحساب، ما عليك سوى إنشاء بعض بيانات الاعتماد. في وحدة تحكّم السحابة الإلكترونية، ابحث عن حساب الخدمة في قائمة حسابات الخدمة، وانقر على النقاط الثلاث العمودية، واختَر إدارة المفاتيح.
- أنشئ مفتاح JSON جديدًا ونزِّله.
- أعِد تسمية الملف الذي تم تنزيله إلى
service-account-google-play.json,
وانقله إلى الدليلassets/
.
هناك إجراء آخر علينا اتّخاذه وهو فتح lib/constants.dart,
واستبدال قيمة androidPackageId
بمعرّف الحزمة الذي اخترته لتطبيق Android.
إعداد إمكانية الوصول إلى Apple 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 '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
الاتصال بنقطة نهاية عملية التحقّق من التطبيق
في التطبيق، أنشئ الدالة _verifyPurchase(PurchaseDetails purchaseDetails)
التي تستدعي نقطة النهاية /verifypurchase
في الخلفية في Dart باستخدام طلب POST لبروتوكول HTTP.
أرسِل المتجر المحدّد (google_play
لمتجر Play أو app_store
لمتجر App Store) وserverVerificationData
وproductID
. يعرض الخادم رمز حالة يشير إلى ما إذا تم إثبات عملية الشراء.
في ثوابت التطبيق، اضبط عنوان IP للخادم على عنوان IP لجهازك.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
إضافة firebaseNotifier
مع إنشاء DashPurchases
في main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
أضِف دالة للحصول على User في FirebaseNotifier، حتى تتمكّن من تمرير معرّف المستخدم إلى دالة verify purchase.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
أضِف الدالة _verifyPurchase
إلى فئة DashPurchases
. تعرض دالة async
هذه قيمة منطقية تشير إلى ما إذا تم التحقّق من عملية الشراء.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
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(1000);
}
}
}
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,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
رائع! لديك الآن معالِجَان للشراء. بعد ذلك، لننشئ نقطة نهاية واجهة برمجة التطبيقات لإثبات ملكية الحساب أثناء عمليات الشراء.
استخدام عناصر معالجة عمليات الشراء
افتح bin/server.dart
وأنشئ نقطة نهاية لواجهة برمجة التطبيقات باستخدام shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.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
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
التحقّق من عمليات الشراء على Android: تنفيذ معالج الشراء
بعد ذلك، واصِل تنفيذ معالِج عمليات الشراء في Google Play.
توفّر Google حاليًا حِزم Dart للتفاعل مع واجهات برمجة التطبيقات التي تحتاج إليها لإثبات ملكية عمليات الشراء. لقد تمّت تهيئتها في ملف server.dart
ويمكنك الآن استخدامها في فئة GooglePlayPurchaseHandler
.
نفِّذ معالِج عمليات الشراء غير من نوع الاشتراكات:
lib/google_play_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
يمكنك تعديل معالِج شراء الاشتراك بطريقة مشابهة:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
أضِف الطريقة التالية لتسهيل تحليل أرقام تعريف الطلبات، بالإضافة إلى طريقتَين لتحليل حالة الشراء.
lib/google_play_purchase_handler.dart
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 {
//..
}
الآن، عليك تنفيذ 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) {
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;
}
}
من المفترض أن يكون قد تم الآن التحقّق من عمليات الشراء في "متجر التطبيقات" وتخزينها في قاعدة البيانات.
تشغيل الخلفية
في هذه المرحلة، يمكنك تشغيل 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 أحداث الفوترة من خلال ما يُعرف باسم موضوع 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
لاستدعاء الطريقة _pullMessageFromSubSub
كل عشر ثوانٍ. يمكنك ضبط المدة حسب تفضيلك.
بعد ذلك، أنشئ _pullMessageFromSubSub
.
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
يتواصل الرمز الذي أضفته للتو مع موضوع Pub/Sub من Google Cloud كل عشر ثوانٍ ويطلب الرسائل الجديدة. بعد ذلك، يعالج كل رسالة في طريقة _processMessage
.
تعمل هذه الطريقة على فك ترميز الرسائل الواردة والحصول على المعلومات المعدَّلة عن كل عملية شراء، سواء كانت اشتراكات أو غير اشتراكات، مع استدعاء handleSubscription
أو handleNonSubscription
الحالية إذا لزم الأمر.
يجب تأكيد كل رسالة باستخدام الطريقة _askMessage
.
بعد ذلك، أضِف التبعيات المطلوبة إلى ملف server.dart
. أضِف PubsubApi.cloudPlatformScope إلى إعدادات بيانات الاعتماد:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
بعد ذلك، أنشئ مثيل PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
وأخيرًا، نقْل القيمة إلى باني GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
إعداد Google Play
لقد كتبت الرمز البرمجي لاستخدام أحداث الفوترة من موضوع النشر/الاشتراك، ولكنك لم تنشئ موضوع النشر/الاشتراك، ولا تنشر أي أحداث فوترة. حان وقت إعداد هذا الإجراء.
أولاً، أنشئ موضوعًا للنشر/الاشتراك:
- انتقِل إلى صفحة Cloud Pub/Sub في Google Cloud Console.
- تأكَّد من أنّك في مشروعك على Firebase، ثم انقر على + إنشاء موضوع.
- أدخِل اسمًا للموضوع الجديد، يكون مطابقًا للقيمة التي تم ضبطها لسمة
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
فيconstants.ts
. في هذه الحالة، يمكنك تسميتهplay_billing
. إذا اخترت خيارًا آخر، احرص على تعديلconstants.ts
. أنشئ الموضوع. - في قائمة مواضيع "النشر/الاشتراك"، انقر على النقاط الثلاث العمودية للموضّع الذي أنشأته للتو، ثم انقر على عرض الأذونات.
- في الشريط الجانبي على يسار الصفحة، اختَر إضافة مشرف.
- أضِف هنا "
google-play-developer-notifications@system.gserviceaccount.com
" وامنحه دور ناشر Pub/Sub. - احفظ التغييرات التي أجريتها على الأذونات.
- انسخ اسم الموضوع الذي أنشأته للتو.
- افتح Play Console مرة أخرى واختَر تطبيقك من قائمة جميع التطبيقات.
- انتقِل للأسفل وانتقِل إلى تحقيق الربح > إعداد تحقيق الربح.
- املأ الموضوع بالكامل واحفظ التغييرات.
سيتم الآن نشر جميع أحداث الفوترة على Google Play في الموضوع.
معالجة أحداث الفوترة في App Store
بعد ذلك، عليك إجراء الخطوات نفسها لأحداث الفوترة في App Store. هناك طريقتان فعّالتان لتنفيذ تحديثات معالجة عمليات الشراء في App Store. أحدهما هو من خلال تنفيذ رابط ويب توفّره لشركة Apple وتستخدمه للتواصل مع خادمك. الطريقة الثانية، وهي الطريقة التي ستعثر عليها في هذا الدليل التعليمي، هي من خلال الاتصال بواجهة برمجة التطبيقات App Store Server API والحصول على معلومات الاشتراك يدويًا.
يركز هذا الدرس التطبيقي حول الترميز على الحلّ الثاني لأنّه عليك إتاحة خادمك على الإنترنت من أجل تنفيذ رابط البيانات.
في بيئة الإنتاج، من الأفضل أن يكون لديك كلا الخيارَين. رابط البيانات في واجهة برمجة التطبيقات للحصول على الأحداث من App Store وServer API في حال فاتتك حدث أو كنت بحاجة إلى التحقّق من حالة الاشتراك.
ابدأ بفتح lib/app_store_purchase_handler.dart
وإضافة التبعية AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
عدِّل الدالة المُنشئة لإضافة موقّت يستدعي طريقة _pullStatus
. سيستدعي هذا الموقّت طريقة _pullStatus
كل 10 ثوانٍ. يمكنك ضبط مدة الموقّت هذه وفقًا لاحتياجاتك.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
بعد ذلك، أنشئ الطريقة _pullStatus على النحو التالي:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
تعمل هذه الطريقة على النحو التالي:
- تحصل على قائمة الاشتراكات النشطة من Firestore باستخدام IapRepository.
- يطلب التطبيق حالة الاشتراك من واجهة برمجة التطبيقات App Store Server API لكل طلب.
- الحصول على آخر معاملة لعملية شراء الاشتراك هذه
- التحقّق من تاريخ انتهاء الصلاحية
- تعديل حالة الاشتراك في Firestore، وفي حال انتهاء صلاحيته، سيتم وضع علامة عليه تشير إلى ذلك
أخيرًا، أضِف كل الرموز البرمجية اللازمة لضبط أذونات الوصول إلى واجهة برمجة التطبيقات App Store Server API:
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
إعداد App Store
بعد ذلك، عليك إعداد App Store:
- سجِّل الدخول إلى 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((DocumentSnapshot document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
يتوفّر كل منطق الشراء في فئة DashPurchases
، وهي المكان الذي يجب فيه تطبيق الاشتراكات أو إزالتها. لذلك، أضِف iapRepo
كسمة في الفئة وحدِّد iapRepo
في الدالة الانشائية. بعد ذلك، أضِف مستمعًا مباشرةً في طريقة الإنشاء، وأزِل المستمع في طريقة dispose()
. في البداية، يمكن أن يكون المستمع دالة فارغة فقط. بما أنّ IAPRepo
هو ChangeNotifier
وأنّك تستدعي notifyListeners()
في كل مرة تتغيّر فيها عمليات الشراء في Firestore، يتم استدعاء طريقة purchasesUpdate()
دائمًا عند تغيير المنتجات التي تم شراؤها.
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
بعد ذلك، قدِّم IAPRepo
إلى الدالة الإنشائية في main.dart.
. يمكنك الحصول على المستودع باستخدام context.read
لأنّه سبق أن تم إنشاؤه في Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
بعد ذلك، اكتب الرمز البرمجي للدالة purchaseUpdate()
. في dash_counter.dart,
، تضبط الطريقتان 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 clicker.
12. أكملت الخطوات بنجاح
تهانينا. لقد أكملت دورة codelab. يمكنك العثور على الرمز البرمجي المكتمل لهذا الدليل التعليمي حول الرموز البرمجية في المجلد complete.
للتعرّف على مزيد من المعلومات، جرِّب ملفات الترميز الأخرى في Flutter codelabs.