1. مقدمة
تاريخ التعديل الأخير: 11/07/2023
لإضافة عمليات شراء داخل التطبيق إلى تطبيق Flutter، يجب إعداد التطبيق ومتاجر Play بشكل صحيح والتأكّد من عملية الشراء ومنح الأذونات اللازمة، مثل مزايا الاشتراك.
في هذا الدرس التطبيقي حول الترميز، ستضيف ثلاثة أنواع من عمليات الشراء داخل التطبيق (المتوفّرة لك) وستتحقّق من عمليات الشراء هذه باستخدام خلفية Dart مع Firebase. يحتوي التطبيق المقدَّم، Dash Clicker، على لعبة تستخدم رمز Dash كعملة. ستضيف خيارات الشراء التالية:
- خيار شراء قابل للتكرار لـ 2000 شرط في آن واحد.
- عملية شراء ترقية لمرة واحدة لتحويل لوحة القيادة من النمط القديم إلى لوحة مفاتيح عصرية.
- اشتراك يضاعف النقرات التي يتم إنشاؤها تلقائيًا.
يمنح خيار الشراء الأول المستخدم ميزة مباشرة لـ 2000 شرط. تتوفر هذه البطاقات للمستخدمين مباشرةً ويمكن شراؤها عدة مرات. ويسمى هذا الاستهلاك نوعًا من الاستهلاك حيث يتم استهلاكه بشكل مباشر ويمكن استهلاكه عدة مرات.
يعمل الخيار الثاني على ترقية لوحة البيانات إلى لوحة أكثر جمالاً. يجب شراء هذه الحزمة مرة واحدة فقط وستظل متاحة إلى الأبد. ويُطلق على عملية الشراء هذه اسم غير قابل للاستهلاك لأنّه لا يمكن للتطبيق استهلاكها ولكنها صالحة إلى الأبد.
إنّ خيار الشراء الثالث والأخير هو الاشتراك. عندما يكون الاشتراك نشطًا، سيحصل المستخدم على Dashes بسرعة أكبر، ولكن عندما يتوقف عن الدفع مقابل الاشتراك، ستختفي المزايا أيضًا.
تعمل خدمة الخلفية (المتوفرة أيضًا لك) كتطبيق Dart، وتتأكّد من إتمام عمليات الشراء، وتخزّنها باستخدام Firestore. تُستخدَم Firestore لتسهيل هذه العملية، ولكن في تطبيق الإنتاج، يمكنك استخدام أي نوع من خدمات الخلفية.
ما الذي ستقوم ببنائه
- سيتم توسيع نطاق التطبيق ليشمل عمليات الشراء والاشتراكات الاستهلاكية.
- ويمكنك أيضًا توسيع نطاق تطبيق واجهة Dart الخلفية للتحقّق من العناصر التي تم شراؤها وتخزينها.
ما الذي ستتعلّمه
- كيفية ضبط App Store و"متجر Play" مع المنتجات القابلة للشراء.
- كيفية التواصل مع المتاجر لتأكيد عمليات الشراء وتخزينها في Firestore
- كيفية إدارة عمليات الشراء في تطبيقك
المتطلبات
- الإصدار 4.1 من "استوديو Android" أو إصدار أحدث
- Xcode 12 أو إصدار أحدث (لتطوير iOS)
- Flutter SDK
2. إعداد بيئة التطوير
لبدء هذا الدرس التطبيقي حول الترميز، نزِّل الرمز وغيِّر معرّف الحزمة لنظام التشغيل iOS واسم الحزمة لنظام التشغيل Android.
تنزيل الرمز
لاستنساخ مستودع جيت هب من سطر الأوامر، استخدم الأمر التالي:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
أو إذا كانت لديك أداة cli في GitHub مثبّتة، استخدِم الأمر التالي:
gh repo clone flutter/codelabs flutter-codelabs
يتم نسخ نموذج الرمز البرمجي إلى دليل flutter-codelabs
يحتوي على الرمز البرمجي لمجموعة من الدروس التطبيقية حول الترميز. يتوفّر رمز هذا الدرس التطبيقي حول الترميز باللغة flutter-codelabs/in_app_purchases
.
تحتوي بنية الدليل ضمن flutter-codelabs/in_app_purchases
على سلسلة من اللقطات للمكان الذي يجب أن تكون فيه في نهاية كل خطوة مُعنوَنة. تجد رمز البدء في الخطوة 0، لذا فإن تحديد موقع الملفات المطابقة أمر سهل مثل:
cd flutter-codelabs/in_app_purchases/step_00
إذا كنت تريد التخطي للأمام أو مشاهدة ما يجب أن يبدو عليه بعد خطوة ما، فابحث في الدليل الذي يحمل اسم الخطوة التي تريدها. يمكنك العثور على رمز الخطوة الأخيرة ضمن المجلد complete
.
إعداد مشروع التفعيل
افتح مشروع إجراء التفعيل من "step_00
" في بيئة تطوير البرامج (IDE) المفضّلة لديك. استخدمنا "استوديو Android" لأخذ لقطات الشاشة، لكنّ Visual Studio Code هو أيضًا خيار رائع. باستخدام أي من المحرِّرين، تأكّد من تثبيت أحدث مكوّنَين إضافيَين لتطبيقَي Dart وFlutter
يجب التواصل مع التطبيقات التي ستنشئها مع App Store و"متجر Play" لمعرفة المنتجات المتوفّرة وسعرها. يتم تحديد كل تطبيق من خلال معرّف فريد. يُعرف ذلك باسم معرّف الحزمة بالنسبة إلى App Store على أجهزة iOS، وهذا هو معرّف التطبيق في "متجر Play" على أجهزة Android. ويتم إنشاء هذه المعرّفات عادةً باستخدام تدوين اسم مجال عكسي. على سبيل المثال، عند إنشاء تطبيق للشراء داخل التطبيق من أجل flutter.dev، سنستخدم dev.flutter.inapppurchase
. فكر في معرف لتطبيقك، ستقوم الآن بتعيينه في إعدادات المشروع.
أولاً، يجب إعداد معرّف الحزمة لنظام التشغيل iOS.
بعد فتح المشروع في "استوديو Android"، انقر بزر الماوس الأيمن على مجلد iOS، ثم انقر على Flutter، وافتح الوحدة في تطبيق Xcode.
في بنية مجلد Xcode، يظهر مشروع Runner في أعلى الصفحة، بينما تظهر الأهداف Flutter وRunner وProducts أسفل مشروع Runner. انقر مرّتين على Runner لتعديل إعدادات المشروع، ثم انقر على توقيع الإمكانات. أدخل معرّف الحزمة الذي اخترته للتو ضمن حقل الفريق لتحديد فريقك.
ويمكنك الآن إغلاق Xcode والرجوع إلى "استوديو Android" لإكمال عملية الإعداد على جهاز Android. لإجراء ذلك، افتح ملف build.gradle
ضمن android/app,
وغيِّر applicationId
(في السطر 37 في لقطة الشاشة أدناه) إلى معرّف التطبيق، وهو نفسه معرّف حزمة iOS. يُرجى العلم أنّه من غير الضروري أن تكون معرّفات متاجر iOS وAndroid متطابقة، إلا أنّ إبقائها متطابقة أقل عرضة للخطأ، وبالتالي سنستخدم أيضًا معرّفات متطابقة في هذا الدرس التطبيقي حول الترميز.
3- تثبيت المكوّن الإضافي
في هذا الجزء من الدرس التطبيقي حول الترميز، ستثبِّت المكوّن الإضافي in_app_purchase.
إضافة تبعية في pubspec
أضف in_app_purchase
إلى pubspecs عن طريق إضافة in_app_purchase
إلى التبعيات في pubspec:
$ cd app $ flutter pub add in_app_purchase
pubspec.yaml
dependencies:
..
cloud_firestore: ^4.0.3
firebase_auth: ^4.2.2
firebase_core: ^2.5.0
google_sign_in: ^6.0.1
http: ^0.13.4
in_app_purchase: ^3.0.1
intl: ^0.18.0
provider: ^6.0.2
..
انقر على pub get لتنزيل الحزمة أو شغِّل flutter pub get
في سطر الأوامر.
4. إعداد App Store
لإعداد عمليات الشراء داخل التطبيق واختبارها على نظام التشغيل iOS، عليك إنشاء تطبيق جديد في App Store وإنشاء منتجات قابلة للشراء هناك. ليس عليك نشر أي تطبيق أو إرساله إلى Apple لمراجعته. لتنفيذ هذا الإجراء، يجب أن يكون لديك حساب مطوِّر. إذا لم يكن لديك حساب، سجِّل في برنامج مطوّري برامج Apple.
اتفاقيات التطبيقات المدفوعة
لاستخدام عمليات الشراء داخل التطبيق، يجب أيضًا أن يكون لديك اتفاقية نشطة للتطبيقات المدفوعة في App Store Connect. انتقِل إلى https://appstoreconnect.apple.com/، وانقر على الاتفاقيات والضرائب والمعاملات المصرفية.
ستظهر لك الاتفاقيات هنا للتطبيقات المجانية والمدفوعة. يجب أن تكون حالة التطبيقات المجانية نشطة، وتكون حالة التطبيقات المدفوعة جديدة. احرص على الاطّلاع على البنود وقبولها وإدخال جميع المعلومات المطلوبة.
عند ضبط كل شيء بشكل صحيح، ستكون حالة التطبيقات المدفوعة نشطة. وهذا أمر مهم جدًا لأنك لن تتمكن من تجربة عمليات الشراء داخل التطبيق بدون اتفاقية نشطة.
تسجيل رقم تعريف التطبيق
أنشِئ معرّفًا جديدًا في بوابة مطوّري برامج Apple.
اختيار أرقام تعريف التطبيقات
اختيار تطبيق
قدِّم بعض الأوصاف وحدِّد معرِّف الحزمة لمطابقة معرِّف الحزمة مع القيمة نفسها التي تم ضبطها سابقًا في XCode.
لمزيد من الإرشادات عن كيفية إنشاء رقم تعريف تطبيق جديد، يُرجى الاطّلاع على مساعدة حساب المطوِّر .
إنشاء تطبيق جديد
أنشِئ تطبيقًا جديدًا في App Store Connect باستخدام معرّف الحزمة الفريد.
للحصول على مزيد من الإرشادات حول كيفية إنشاء تطبيق جديد وإدارة الاتفاقيات، يُرجى الاطّلاع على مركز مساعدة App Store Connect.
لاختبار عمليات الشراء داخل التطبيق، تحتاج إلى مستخدم تجريبي في وضع الحماية. يجب عدم ربط هذا المستخدم التجريبي بـ iTunes، فهو يُستخدم فقط لاختبار عمليات الشراء داخل التطبيق. لا يمكنك استخدام عنوان بريد إلكتروني مُستخدَم حاليًا في حساب Apple. في صفحة المستخدمون والوصول، انتقِل إلى المختبِرون ضمن وضع الحماية لإنشاء حساب وضع حماية جديد أو إدارة معرّفات Apple الحالية في وضع الحماية.
يمكنك الآن إعداد مستخدم وضع الحماية على جهاز iPhone من خلال الانتقال إلى الإعدادات > App Store > Sandbox-account
ضبط عمليات الشراء داخل التطبيق
ستقوم الآن بتهيئة العناصر الثلاثة القابلة للشراء:
dash_consumable_2k
: عملية شراء استهلاكية يمكن شراؤها عدة مرات، وتمنح المستخدم 2000 شرط (العملة داخل التطبيق) لكل عملية شراء.dash_upgrade_3d
: "ترقية" غير قابلة للاستهلاك عملية الشراء التي لا يمكن شراؤها سوى مرة واحدة، وتمنح المستخدم لوحة تحكم مختلفة من الناحية الجمالية للنقر عليها.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
. انقر على حفظ (Save).
بعد النقر على الزر حفظ، أضِف سعر الاشتراك. اختَر أي سعر تريده.
من المفترض أن تظهر الآن عمليات الشراء الثلاث في قائمة المشتريات:
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
file Private؛ يُرجى عدم التحقّق من ذلك في مراقبة المصادر العامة.
الإشارة إلى ملف تخزين المفاتيح من التطبيق
أنشئ ملفًا باسم "<your app dir>/android/key.properties
" يحتوي على مرجع إلى ملف تخزين المفاتيح:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
ضبط إعدادات Gradle لتسجيل الدخول
يمكنك ضبط عملية تسجيل الدخول لتطبيقك من خلال تعديل ملف <your app dir>/android/app/build.gradle
.
أضِف معلومات ملف تخزين المفاتيح من ملف مواقعك قبل حظر android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
حمِّل ملف key.properties
إلى الكائن keystoreProperties
.
أضِف الرمز التالي قبل مجموعة buildTypes
:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
اضبط الجزء signingConfigs
في ملف build.gradle
الخاص بالوحدة باستخدام معلومات إعداد التوقيع:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
سيتم الآن توقيع إصدارات إصدارات تطبيقك تلقائيًا.
لمزيد من المعلومات حول توقيع تطبيقك، يمكنك الاطّلاع على توقيع تطبيقك على developer.android.com.
تحميل الإصدار الأول
بعد إعداد تطبيقك للتوقيع، من المفترض أن تتمكّن من إنشاء تطبيقك من خلال تنفيذ ما يلي:
flutter build appbundle
ينشئ هذا الأمر إصدار إصدار تلقائيًا ويمكن العثور على المخرجات في <your app dir>/build/app/outputs/bundle/release/
من لوحة البيانات في Google Play Console، انتقِل إلى الإصدار > الاختبار > الاختبار المغلق وإنشاء إصدار جديد للاختبار المغلق
في هذا الدرس التطبيقي، عليك الالتزام بتوقيع التطبيق من خلال Google، لذا اضغط على متابعة ضمن ميزة "توقيع التطبيق" من Play للموافقة.
بعد ذلك، يجب تحميل حِزمة تطبيق "app-release.aab
" التي تم إنشاؤها بواسطة أمر الإصدار.
انقر على حفظ ثم على مراجعة إصدار التطبيق.
أخيرًا، انقر على بدء طرح الإصدار للاختبار الداخلي لتفعيل إصدار الاختبار الداخلي.
إعداد المستخدمين الاختباريين
لكي تتمكّن من اختبار عمليات الشراء داخل التطبيق، يجب إضافة حسابات Google للمختبِرين إلى وحدة تحكُّم Google Play في مكانين:
- إلى مسار الاختبار المحدد (الاختبار الداخلي)
- بصفتك مختبِرًا للترخيص
أولاً، ابدأ بإضافة المختبِر إلى مسار الاختبار الداخلي. الرجوع إلى الإصدار > الاختبار > الاختبار الداخلي وانقر على علامة التبويب المختبِرون.
أنشئ قائمة عناوين بريد إلكتروني جديدة بالنقر على إنشاء قائمة عناوين بريد إلكتروني. أدخِل اسمًا للقائمة، ثم أضِف عناوين البريد الإلكتروني لحسابات Google التي تحتاج إلى إذن لاختبار عمليات الشراء داخل التطبيق.
بعد ذلك، ضَع علامة في مربّع الاختيار للقائمة، وانقر على حفظ التغييرات.
بعد ذلك، أضِف مختبِري الترخيص:
- ارجع إلى عرض جميع التطبيقات في Google Play Console.
- انتقِل إلى الإعدادات > اختبار الترخيص:
- يُرجى إضافة عناوين البريد الإلكتروني نفسها للمختبِرين المطلوب منهم السماح لهم باختبار عمليات الشراء داخل التطبيق.
- اضبط ردّ الترخيص على
RESPOND_NORMALLY
. - انقر على حفظ التغييرات.
ضبط عمليات الشراء داخل التطبيق
ستقوم الآن بتكوين العناصر التي يمكن شراؤها داخل التطبيق.
وكما هو الحال في App Store، يجب تحديد ثلاث عمليات شراء مختلفة:
dash_consumable_2k
: عملية شراء استهلاكية يمكن شراؤها عدة مرات، وتمنح المستخدم 2000 شرط (العملة داخل التطبيق) لكل عملية شراء.dash_upgrade_3d
: "ترقية" غير قابلة للاستهلاك عملية الشراء التي لا يمكن شراؤها سوى مرة واحدة، مما يمنح المستخدم لوحة تحكم مختلفة من الناحية الجمالية للنقر عليها.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، اختَر المشروع الذي أنشأته للتوّ في الخطوة السابقة.
$ 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
". أو مع بيئة تطوير متكاملة (IDE) تختارها.
في VSCode، انقر بزر الماوس الأيمن على المجلد ios/
، ثم على open in xcode
.
في "استوديو Android"، انقر بزر الماوس الأيمن على مجلد ios/
ثم انقر على flutter
متبوعًا بالخيار open iOS module in Xcode
.
للسماح بتسجيل الدخول إلى حساب Google على نظام التشغيل iOS، أضِف خيار الضبط CFBundleURLTypes
إلى نسخة plist
من الملفات. (يُرجى الاطّلاع على مستندات حزمة google_sign_in
للحصول على مزيد من المعلومات). في هذه الحالة، يكون الملفان ios/Runner/Info-Debug.plist
وios/Runner/Info-Release.plist
.
تمت إضافة زوج المفتاح/القيمة من قبل، ولكن يجب استبدال القيم الخاصة بهما:
- يمكنك الحصول على قيمة
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,
),
يجب أيضًا توفير نسخة افتراضية من InAppPurchaseConnection
. ومع ذلك، لإبقاء التطبيق قابلاً للاختبار، تحتاج إلى طريقة ما لمحاكاة الاتصال. لإجراء ذلك، يمكنك إنشاء طريقة مثيل يمكن تجاوزها في الاختبار وإضافتها إلى السمة main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
يجب تعديل الاختبار قليلاً إذا أردت مواصلة عمله. راجع widget_test.dart على GitHub للحصول على الرمز الكامل الخاص بـ TestIAPConnection
.
test/widget_test.dart
void main() {
testWidgets('App starts', (WidgetTester tester) async {
IAPConnection.instance = TestIAPConnection();
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
في lib/logic/dash_purchases.dart
، انتقِل إلى رمز DashPurchases ChangeNotifier
. في الوقت الحالي، يمكنك إضافة DashCounter
فقط إلى الشرطات التي اشتريتها.
أضِف خاصية الاشتراك في البث، _subscription
(من النوع StreamSubscription<List<PurchaseDetails>> _subscription;
)، وIAPConnection.instance,
وعمليات الاستيراد. يجب أن تبحث التعليمة البرمجية الناتجة عن ما يلي:
lib/logic/dash_purchases.dart
import 'package:in_app_purchase/in_app_purchase.dart';
class DashPurchases extends ChangeNotifier {
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter);
}
تمت إضافة الكلمة الرئيسية late
إلى _subscription
بسبب إعداد _subscription
في الدالة الإنشائية. تم إعداد هذا المشروع ليكون غير قابل للقيم الفارغة بشكل تلقائي (NNBD)، ما يعني أنّ الخصائص التي لم يتم الإعلان عنها قابلة للقيم الفارغة يجب أن تحتوي على قيمة غير فارغة. يتيح لك مؤهِّل late
تأخير تحديد هذه القيمة.
في الدالة الإنشائية، احصل على purchaseUpdatedStream
وابدأ الاستماع إلى ساحة المشاركات. باستخدام طريقة dispose()
، ألغِ الاشتراك في البث.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
Future<void> buy(PurchasableProduct product) async {
// omitted
}
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
}
}
يتلقّى التطبيق الآن تحديثات الشراء، لذا ستُجري عملية شراء في القسم التالي.
قبل المتابعة، يمكنك إجراء الاختبارات باستخدام "flutter test"
" للتأكّد من أنّه تم إعداد كل شيء بشكل صحيح.
$ flutter test
00:01 +1: All tests passed!
8. إجراء عمليات شراء
في هذا الجزء من الدرس التطبيقي حول الترميز، ستستبدل المنتجات الوهمية الحالية حاليًا بمنتجات حقيقية قابلة للشراء. ويتمّ تحميل هذه المنتجات من المتاجر وعرضها في قائمة، ويتم شراؤها عند النقر على المنتج.
تعديل المنتج القابل للشراء
يعرض PurchasableProduct
منتجًا وهميًا. يُرجى تعديله لعرض المحتوى الفعلي من خلال استبدال الصف PurchasableProduct
في purchasable_product.dart
بالرمز التالي:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
إزالة عمليات الشراء الوهمية في "dash_purchases.dart,
" واستبدالها بقائمة فارغة، List<PurchasableProduct> products = [];
تحميل عمليات الشراء المتاحة
لمنح المستخدم إمكانية إجراء عملية شراء، يجب تحميل عمليات الشراء من المتجر. تحقّق أولاً ممّا إذا كان المتجر متوفرًا. عندما لا يكون المتجر متوفّرًا، يؤدي ضبط storeState
على notAvailable
إلى عرض رسالة خطأ للمستخدم.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
عندما يكون المتجر متاحًا، يمكنك تحميل عمليات الشراء المتاحة. استنادًا إلى إعداد Firebase السابق، توقَّع ظهور storeKeyConsumable
وstoreKeySubscription,
وstoreKeyUpgrade
. عندما لا تتوفر عملية شراء متوقعة، اطبع هذه المعلومات على وحدة التحكم؛ قد ترغب أيضًا في إرسال هذه المعلومات إلى الخدمة الخلفية.
تعرض الطريقة await iapConnection.queryProductDetails(ids)
كلاً من المعرّفات التي لم يتم العثور عليها والمنتجات القابلة للشراء التي تم العثور عليها. استخدِم productDetails
في الردّ لتعديل واجهة المستخدم، واضبط StoreState
على available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
for (var element in response.notFoundIDs) {
debugPrint('Purchase $element not found');
}
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
استدعِ الدالة loadPurchases()
في الدالة الإنشائية:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
أخيرًا، غيِّر قيمة الحقل storeState
من StoreState.available
إلى StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
عرض المنتجات القابلة للشراء
ضع في الاعتبار الملف purchase_page.dart
. تعرض أداة "PurchasePage
" _PurchasesLoading
أو _PurchaseList,
أو _PurchasesNotAvailable,
حسب StoreState
. وتعرض الأداة أيضًا عمليات الشراء السابقة للمستخدم، والتي يتم استخدامها في الخطوة التالية.
يعرض التطبيق المصغّر "_PurchaseList
" قائمة المنتجات القابلة للشراء ويرسل طلب شراء إلى عنصر "DashPurchases
".
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
من المفترض أن تتمكّن من الاطّلاع على المنتجات المتاحة في متجرَي Android وiOS إذا تم إعدادها بشكلٍ صحيح. يُرجى العِلم أنّ إتاحة عمليات الشراء عند إدخالها في وحدات التحكّم المعنية قد تستغرق بعض الوقت.
ارجع إلى dash_purchases.dart
، ونفِّذ الدالة لشراء منتج. ما عليك سوى فصل المواد الاستهلاكية عن السلع غير الاستهلاكية. يُرجى العلم أنّ منتجات الترقية والمنتجات المتوفّرة عند الاشتراك هي منتجات غير استهلاكية.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
break;
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
قبل المتابعة، أنشِئ المتغيّر _beautifiedDashUpgrade
وعدِّل دالة beautifiedDash
للإشارة إليه.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
تتلقّى طريقة _onPurchaseUpdate
آخر المعلومات المتعلقة بعملية الشراء، وتعدّل حالة المنتج الذي يظهر في صفحة الشراء، وتطبّق عملية الشراء على منطق الطلب. من المهم الاتصال بالرقم completePurchase
بعد معالجة عملية الشراء حتى يتأكد المتجر من إتمام عملية الشراء بشكل صحيح.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(2000);
break;
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. إعداد الواجهة الخلفية
قبل الانتقال إلى تتبُّع عمليات الشراء والتحقّق منها، يمكنك إعداد واجهة Dart الخلفية لإتاحة ذلك.
في هذا القسم، اعمل من المجلد dart-backend/
باعتباره الجذر.
تأكد من تثبيت الأدوات التالية:
نظرة عامة على المشروع الأساسي
ولأنّ بعض أجزاء هذا المشروع تُعد خارج نطاق هذا الدرس التطبيقي حول الترميز، يتم تضمينها في رمز إجراء التفعيل. ومن المفيد مراجعة ما هو موجود بالفعل في شفرة البداية قبل أن تبدأ، وذلك للتعرف على كيفية هيكلة الأمور.
يمكن تشغيل رمز الخلفية هذا محليًا على جهازك، ولن تحتاج إلى نشره لاستخدامه. ومع ذلك، يجب أن تكون قادرًا على الاتصال من جهاز التطوير (Android أو iPhone) بالجهاز الذي سيتم تشغيل الخادم عليه. لإجراء ذلك، يجب أن يكونا متصلَين بالشبكة نفسها، وعليك معرفة عنوان IP لجهازك.
جرب تشغيل الخادم باستخدام الأمر التالي:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
تستخدم الواجهة الخلفية Dart shelf
وshelf_router
لعرض نقاط نهاية واجهة برمجة التطبيقات. لا يوفر الخادم أي مسارات بشكل تلقائي. سيتم في وقت لاحق إنشاء مسار لمعالجة عملية إثبات ملكية الحساب أثناء عمليات الشراء.
وأحد الأجزاء التي سبق تضمينها في رمز التفعيل هو IapRepository
في lib/iap_repository.dart
. إنّ تعلُّم كيفية التفاعل مع Firestore، أو قواعد البيانات بشكل عام، ليس ذا صلة بهذا الدرس التطبيقي حول الترميز، لذلك يحتوي الرمز البرمجي للمبتدئين على وظائف تتيح لك إنشاء عمليات الشراء أو تعديلها في Firestore، بالإضافة إلى جميع فئات عمليات الشراء هذه.
إعداد الوصول إلى Firebase
للوصول إلى Firebase Firestore، يجب استخدام مفتاح وصول لحساب الخدمة. أنشئ مفتاحًا لفتح إعدادات مشروع Firebase وانتقِل إلى قسم حسابات الخدمة، ثم اختَر إنشاء مفتاح خاص جديد.
انسخ ملف JSON الذي تم تنزيله إلى مجلد "assets/
"، ثم أعِد تسميته إلى service-account-firebase.json
.
إعداد إمكانية الوصول إلى Google Play
للوصول إلى "متجر Play" لتأكيد عمليات الشراء، يجب إنشاء حساب خدمة باستخدام هذه الأذونات وتنزيل بيانات اعتماد JSON الخاصة به.
- انتقِل إلى Google Play Console، وابدأ من صفحة جميع التطبيقات.
- الانتقال إلى إعداد > الوصول إلى واجهة برمجة التطبيقات: إذا طلبت أداة Google Play Console إنشاء مشروع حالي أو الربط بمشروع حالي، عليك إجراء ذلك أولاً ثم العودة إلى هذه الصفحة.
- ابحث عن القسم الذي يمكنك من خلاله تحديد حسابات الخدمة، وانقر على إنشاء حساب خدمة جديد.
- انقر على رابط Google Cloud Platform في مربّع الحوار المنبثق.
- اختَر مشروعك. إذا لم يظهر لك هذا الخيار، فتأكد من تسجيل الدخول إلى حساب Google الصحيح ضمن القائمة المنسدلة الحساب في أعلى اليسار.
- بعد اختيار مشروعك، انقر على + إنشاء حساب خدمة في شريط القوائم العلوي.
- أدخِل اسمًا لحساب الخدمة، ويمكنك اختياريًا إدخال وصف حتى تتمكّن من تذكُّره والانتقال إلى الخطوة التالية.
- خصِّص حساب الخدمة دور المحرِّر.
- أنهِ المعالج، ثم ارجع إلى صفحة الوصول إلى واجهة برمجة التطبيقات ضمن Play Console، وانقر على إعادة تحميل حسابات الخدمة. من المفترض أن يظهر لك الحساب الجديد في القائمة.
- انقر على منح إذن الوصول لحساب الخدمة الجديد.
- انتقِل إلى أسفل الصفحة التالية وصولاً إلى مجموعة البيانات المالية. اختَر كلاً من عرض البيانات المالية والطلبات والردود على استطلاعات أسباب الإلغاء وإدارة الطلبات والاشتراكات.
- انقر على دعوة مستخدم.
- الآن وبعد إعداد الحساب، ما عليك سوى إنشاء بعض بيانات الاعتماد. في Cloud Console، ابحث عن حساب الخدمة الخاص بك في قائمة حسابات الخدمة، ثم انقر على النقاط الرأسية الثلاث، واختَر إدارة المفاتيح.
- أنشئ مفتاح JSON جديدًا ونزِّله.
- أعِد تسمية الملف الذي تم تنزيله إلى
service-account-google-play.json,
وانقله إلى دليلassets/
.
هناك إجراء آخر علينا اتّخاذه وهو فتح 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 '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
نقطة نهاية التحقّق من المكالمات من التطبيق
في التطبيق، أنشِئ الدالة _verifyPurchase(PurchaseDetails purchaseDetails)
التي تستدعي نقطة النهاية /verifypurchase
على خلفية Dart باستخدام مكالمة http بعد انتهاء العملية.
أرسِل المتجر الذي اخترته (google_play
مقابل "متجر Play" أو app_store
لتطبيق App Store) وserverVerificationData
وproductID
. يعرض الخادم رمز الحالة الذي يشير إلى ما إذا تم تأكيد عملية الشراء.
في ثوابت التطبيق، يمكنك ضبط عنوان IP للخادم على عنوان IP لجهازك المحلي.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
إضافة firebaseNotifier
مع إنشاء DashPurchases
في main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
أضِف قيمة getter للمستخدم في FirebaseNotifier، حتى تتمكّن من تمرير رقم تعريف المستخدم إلى وظيفة "التحقُّق من الشراء".
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
أضِف الدالة _verifyPurchase
إلى الفئة DashPurchases
. تعرض الدالة async
هذه قيمة منطقية تشير إلى ما إذا تم التحقّق من صحة عملية الشراء.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
print('Successfully verified purchase');
return true;
} else {
print('failed request: ${response.statusCode} - ${response.body}');
return false;
}
}
يمكنك طلب الدالة _verifyPurchase
في _handlePurchase
قبل تطبيق عملية الشراء مباشرةً. يجب عدم تطبيق عملية الشراء إلا بعد إثبات ملكيتها. وفي تطبيق علني، يمكنك تحديد ذلك أيضًا لتطبيق اشتراك تجريبي عندما يكون المتجر غير متاح مؤقتًا مثلاً. مع ذلك، في هذا المثال، اجعل عملية الشراء بسيطة، ولا تطبِّق عملية الشراء إلا بعد إثبات ملكية عملية الشراء بنجاح.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(1000);
break;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
أصبح كل شيء في التطبيق جاهزًا الآن للتحقق من عمليات الشراء.
إعداد خدمة الخلفية
بعد ذلك، قم بإعداد وظيفة السحابة للتحقق من عمليات الشراء في الخلفية.
إنشاء معالِجات الشراء
بما أنّ خطوات إثبات الملكية في كلا المتجرَين متقاربة، يجب إعداد فئة PurchaseHandler
مختصرة مع عمليات تنفيذ منفصلة لكلّ متجر.
ابدأ بإضافة ملف purchase_handler.dart
إلى المجلد lib/
، حيث يمكنك تحديد صف PurchaseHandler
موجز بطريقتَين مختصرتَين للتحقّق من نوعَين مختلفَين من عمليات الشراء: الاشتراكات وغير الاشتراكات.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
كما ترى، تتطلب كل طريقة ثلاث معلمات:
userId:
رقم تعريف المستخدم الذي سجَّل الدخول، حتى تتمكّن من ربط عمليات الشراء بالمستخدمproductData:
بيانات عن المنتج ستحدد ذلك خلال دقيقة.token:
الرمز المميّز الذي وفّره المتجر للمستخدم
ولتسهيل استخدام معالِجات الشراء هذه، يمكنك أيضًا إضافة طريقة verifyPurchase()
يمكن استخدامها للاشتراكات وغير المرتبطة بالاشتراكات:
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
يمكنك الآن فقط طلب الرقم verifyPurchase
لكلتا الحالتين، ولكن لا يزال هناك عمليات تنفيذ منفصلة.
تحتوي الفئة ProductData
على معلومات أساسية حول المنتجات المختلفة التي يمكن شراؤها، بما في ذلك معرّف المنتج (ويُشار إليه أحيانًا أيضًا باسم رمز التخزين التعريفي) وProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ويمكن أن يكون ProductType
اشتراكًا أو غير اشتراك.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
أخيرًا، يتم تعريف قائمة المنتجات كخريطة في الملف نفسه.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
بعد ذلك، حدِّد بعض عمليات تنفيذ العناصر النائبة لـ "متجر Google Play" وApple App Store. البدء بـ Google Play:
يمكنك إنشاء "lib/google_play_purchase_handler.dart
" وإضافة صف يوسِّع نطاق PurchaseHandler
الذي كتبته للتو:
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
وفي الوقت الحالي، تعرض الدالة true
لطرق المعالجات؛ ستصل إليها لاحقًا.
وكما لاحظت، تأخذ الدالة الإنشائية مثيلاً للدالة IapRepository
. يستخدم معالج الشراء هذا المثيل لتخزين معلومات عن عمليات الشراء في Firestore لاحقًا. للتواصل مع Google Play، يمكنك استخدام AndroidPublisherApi
المقدّم.
بعد ذلك، افعل الشيء نفسه مع معالج متجر التطبيقات. أنشِئ lib/app_store_purchase_handler.dart
، ثم أضِف فئة توسّع PurchaseHandler
مرة أخرى:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
رائع! الآن لديك معالجان للشراء. بعد ذلك، سننشئ نقطة نهاية واجهة برمجة التطبيقات لتأكيد عمليات الشراء.
استخدام معالِجات الشراء
افتح bin/server.dart
وأنشئ نقطة نهاية لواجهة برمجة التطبيقات باستخدام shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
يقوم الرمز البرمجي أعلاه بما يلي:
- حدد نقطة نهاية 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
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
من المفترض الآن أن يتم إثبات ملكية عمليات الشراء التي أجريتها على Google Play وتخزينها في قاعدة البيانات.
بعد ذلك، انتقِل إلى عمليات الشراء في App Store على أجهزة iOS.
التأكّد من عمليات الشراء على أجهزة iOS: تنفيذ معالِج الشراء
لتأكيد عمليات الشراء باستخدام App Store، تتوفّر حزمة Dart تابعة لجهة خارجية اسمها app_store_server_sdk
، ما يسهّل هذه العملية.
ابدأ بإنشاء مثيل ITunesApi
. استخدام تهيئة وضع الحماية، فضلاً عن تفعيل التسجيل لتسهيل تصحيح الأخطاء.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
وعلى عكس واجهات برمجة تطبيقات Google Play، يستخدم متجر App Store الآن نقاط نهاية واجهة برمجة التطبيقات نفسها لكلّ من الاشتراكات وغير الاشتراكات. وهذا يعني أنه يمكنك استخدام المنطق نفسه لكلا المعالجين. ادمجهما معًا لاستدعاء عملية التنفيذ نفسها:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
الآن، نفِّذ handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
من المفترض الآن أن يتم التحقق من عمليات الشراء التي تم إجراؤها في App Store وتخزينها في قاعدة البيانات.
تشغيل الخلفية
في هذه المرحلة، يمكنك تشغيل dart bin/server.dart
لعرض نقطة النهاية /verifypurchase
.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. تتبُّع عمليات الشراء
الطريقة الموصى بها لتتبع زيارات المستخدمين عمليات الشراء في الخدمة الخلفية. وذلك لأنّ الخلفية يمكن أن تستجيب للأحداث من المتجر وبالتالي تكون أقل عرضة للوقوع في معلومات قديمة بسبب التخزين المؤقت، فضلاً عن كونها أقل عرضة للتلاعب بها.
أولاً، عليك إعداد معالجة أحداث المتجر في الخلفية باستخدام واجهة Dart التي كنت تبنيها.
معالجة أحداث المتجر في الخلفية
يمكن للمتاجر إعلام الخلفية بأي أحداث فوترة تحدث، مثلاً عند تجديد الاشتراكات. يمكنك معالجة هذه الأحداث في الخلفية لتحديث عمليات الشراء في قاعدة البيانات لديك. في هذا القسم، يمكنك إعداد هذا لكل من "متجر Google Play" وApple App Store.
معالجة أحداث "الفوترة في Google Play"
يوفّر Google Play أحداث الفوترة من خلال ما يُطلق عليه موضوع النشر/الفرع في السحابة الإلكترونية. وتكون هذه في الأساس قوائم انتظار للرسائل يمكن نشر الرسائل عليها وكذلك استهلاكها.
ولأنّ هذه الوظيفة خاصة بـ Google Play، يجب تضمين هذه الوظيفة في GooglePlayPurchaseHandler
.
ابدأ بفتح lib/google_play_purchase_handler.dart
، وإضافة عملية استيراد PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
بعد ذلك، أدخِل PubsubApi
إلى GooglePlayPurchaseHandler
وعدِّل الدالة الإنشائية للفئة لإنشاء Timer
على النحو التالي:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
تم ضبط Timer
لاستدعاء الطريقة _pullMessageFromSubSub
كل عشر ثوانٍ. يمكنك تعديل المدة وفقًا لتفضيلك الخاص.
بعد ذلك، عليك إنشاء _pullMessageFromSubSub
.
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
يتواصل الرمز الذي أضفته للتو مع موضوع النشر/الاشتراك من Google Cloud كل عشر ثوانٍ ويطلب رسائل جديدة. بعد ذلك، تتم معالجة كل رسالة بطريقة _processMessage
.
تعمل هذه الطريقة على فك ترميز الرسائل الواردة والحصول على المعلومات المحدَّثة حول كل عملية شراء، سواء كانت اشتراكات أو اشتراكات أخرى، من خلال الاتصال بالرقم handleSubscription
أو handleNonSubscription
الحالي إذا لزم الأمر.
يجب الإقرار بكل رسالة باستخدام الطريقة _askMessage
.
أضِف بعد ذلك الملحقات المطلوبة إلى ملف server.dart
. أضِف PubsubApi.cloudPlatformScope إلى إعدادات بيانات الاعتماد:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
بعد ذلك، أنشئ مثيل PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
وأخيرًا، ضعه في الدالة الإنشائية GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
إعداد Google Play
لقد كتبت الرمز لاستخدام أحداث الفوترة من موضوع الناشر/الموضوع الفرعي، ولكنك لم تنشئ موضوع الناشر/الموضوع الفرعي، ولا تنشر أي أحداث فوترة. حان الوقت لإعداد هذه الميزة.
أولاً، أنشئ موضوعًا عامًا أو فرعيًا:
- انتقِل إلى صفحة 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
وامنحه دور الناشر/الناشر. - احفظ تغييرات الأذونات.
- انسخ اسم الموضوع للموضوع الذي أنشأته للتو.
- افتح Play Console مرة أخرى واختَر تطبيقك من قائمة جميع التطبيقات.
- مرِّر لأسفل وانتقِل إلى تحقيق الربح > إعداد تحقيق الربح:
- املأ الموضوع بالكامل واحفظ التغييرات.
سيتم الآن نشر جميع أحداث "الفوترة في Google Play" حول هذا الموضوع.
معالجة أحداث الفوترة في App Store
بعد ذلك، نفِّذ الأمر نفسه مع أحداث الفوترة في App Store. هناك طريقتان فعّالتان لتنفيذ التحديثات في المشتريات على App Store. أحدهما من خلال تنفيذ الرد التلقائي على الويب الذي تقدمه إلى Apple واستخدامه للاتصال بخادمك. الطريقة الثانية، وهي الطريقة التي ستجدها في هذا الدرس التطبيقي حول الترميز، هي من خلال الربط بواجهة برمجة تطبيقات App Store Server والحصول على معلومات الاشتراك يدويًا.
وتكمن أهمية هذا الدرس التطبيقي في الترميز على الحل الثاني لأنّه عليك عرض الخادم على الإنترنت لتنفيذ الردّ التلقائي على الويب.
في بيئة إنتاج، يُفضَّل أن يكون لديكما الاثنين معًا. الردّ التلقائي على الويب للحصول على الأحداث من App Store وServer API في حال تفويت حدث أو كنت بحاجة إلى التحقّق جيدًا من حالة الاشتراك.
ابدأ بفتح lib/app_store_purchase_handler.dart
، وإضافة الاعتمادية على AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
عدِّل الدالة الإنشائية لإضافة مؤقت سيتم الاتصال بطريقة _pullStatus
. سيطلب هذا الموقّت استخدام طريقة _pullStatus
كل 10 ثوانٍ. يمكنك تعديل مدة الموقّت حسب احتياجاتك.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
بعد ذلك، قم بإنشاء طريقة _pullStatus على النحو التالي:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
تعمل هذه الطريقة على النحو التالي:
- الحصول على قائمة بالاشتراكات النشطة من Firestore باستخدام IapRepository
- بالنسبة إلى كل طلب، يطلب التطبيق حالة الاشتراك من App Store Server API.
- الحصول على آخر معاملة لعملية شراء الاشتراك هذه
- التحقق من تاريخ انتهاء الصلاحية.
- لتعديل حالة الاشتراك على Firestore، إذا انتهت صلاحيته، سيتم وضع علامة على هذه الحالة تشير إلى ذلك.
أخيرًا، أضِف كل الرموز اللازمة لضبط الوصول إلى واجهة برمجة تطبيقات App Store Server:
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
إعداد App Store
بعد ذلك، عليك إعداد App Store:
- سجِّل الدخول إلى App Store Connect، وانقر على المستخدمون وأذونات الوصول.
- انتقل إلى نوع المفتاح > عملية شراء داخل التطبيق:
- انقر على علامة الجمع لإضافة رمز جديد.
- أدخِل اسمًا لها، على سبيل المثال: "مفتاح درس تطبيقي حول الترميز".
- نزِّل ملف 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() {
iapRepo.removeListener(purchasesUpdate);
_subscription.cancel();
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
بعد ذلك، يمكنك توفير IAPRepo
إلى الدالة الإنشائية في main.dart.
. يمكنك الحصول على المستودع باستخدام context.read
لأنه تم إنشاؤه من قبل في Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
بعد ذلك، اكتب التعليمة البرمجية للدالة purchaseUpdate()
. في dash_counter.dart,
، يتم ضبط المُضاعِف على 10 أو 1 في الطريقتَين applyPaidMultiplier
وremovePaidMultiplier
، على التوالي، كي لا تضطر إلى التحقّق مما إذا كان الاشتراك مطبَّقًا حاليًا. وعند تغيّر حالة الاشتراك، يمكنك أيضًا تعديل حالة المنتج القابل للشراء لإظهار أنّه نشط في صفحة الشراء. اضبط السمة _beautifiedDashUpgrade
استنادًا إلى ما إذا كان يتم شراء الترقية.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
لقد تأكّدت الآن من أنّ حالة الاشتراك والترقية محدَّثة دائمًا في خدمة الخلفية وأنّه تمت مزامنته مع التطبيق. يعمل التطبيق وفقًا لذلك ويطبِّق ميزات الاشتراك والترقية على لعبة النقر في Dash.
12. أكملت كل الإجراءات
تهانينا! لقد أكملت الدرس التطبيقي حول الترميز. يمكنك العثور على الرمز المكتمل لهذا الدرس التطبيقي في المجلد الكامل.
لمزيد من المعلومات، يمكنك تجربة الدروس التطبيقية حول ترميز Flutter.