1. מבוא
כדי להוסיף רכישות מתוך האפליקציה לאפליקציית Flutter, צריך להגדיר נכון את חנויות האפליקציות (App Store ו-Play Store), לאמת את הרכישה ולהעניק את ההרשאות הנדרשות, כמו הטבות למנויים.
ב-codelab הזה תוסיפו לאפליקציה (שמסופקת לכם) שלושה סוגים של רכישות מתוך האפליקציה, ותאמתו את הרכישות האלה באמצעות קצה עורפי של Dart עם Firebase. האפליקציה שצוינה, Dash Clicker, מכילה משחק שמשתמש בקמע Dash כמטבע. תוסיפו את אפשרויות הרכישה הבאות:
- אפשרות רכישה חוזרת של 2,000 Dash בבת אחת.
- רכישת שדרוג חד-פעמי כדי לשנות את לוח הבקרה הישן ללוח בקרה מודרני.
- מינוי שמכפיל את מספר הקליקים שנוצרים אוטומטית.
האפשרות הראשונה לרכישה מעניקה למשתמש הטבה ישירה של 2,000 Dash. הם זמינים ישירות למשתמש ואפשר לקנות אותם הרבה פעמים. המוצר הזה נקרא מוצר מתכלה כי הוא נצרך ישירות ואפשר לצרוך אותו כמה פעמים.
האפשרות השנייה משדרגת את ה-Dash ל-Dash יפה יותר. צריך לרכוש את המינוי הזה רק פעם אחת, והוא זמין לתמיד. רכישה כזו נקראת רכישה לא מתכלה כי אי אפשר לצרוך אותה באפליקציה, אבל היא תקפה לנצח.
אפשרות הרכישה השלישית והאחרונה היא מינוי. בזמן שהמינוי פעיל, המשתמש יקבל את המקפים מהר יותר, אבל כשהוא יפסיק לשלם על המינוי, הוא יאבד את ההטבות.
השירות לקצה העורפי (שמסופק גם הוא) פועל כאפליקציית Dart, מאמת שהרכישות בוצעו ומאחסן אותן באמצעות Firestore. השתמשנו ב-Firestore כדי להקל על התהליך, אבל באפליקציה שלכם בסביבת הייצור אתם יכולים להשתמש בכל סוג של שירות backend.
מה תפַתחו
- תלמדו איך להרחיב אפליקציה כדי לתמוך ברכישות של מוצרים מתכלים ובמינויים.
- בנוסף, תרחיבו אפליקציית Dart backend כדי לאמת ולאחסן את הפריטים שנרכשו.
מה תלמדו
- איך מגדירים את App Store ואת חנות Play עם מוצרים שאפשר לרכוש.
- איך מתקשרים עם החנויות כדי לאמת רכישות ולאחסן אותן ב-Firestore.
- איך לנהל רכישות באפליקציה.
מה צריך?
- Android Studio
- Xcode (לפיתוח ל-iOS)
- Flutter SDK
2. הגדרת סביבת הפיתוח
כדי להתחיל את ה-codelab הזה, מורידים את הקוד ומשנים את מזהה החבילה ל-iOS ואת שם החבילה ל-Android.
הורדת הקוד
כדי לשכפל את מאגר GitHub משורת הפקודה, משתמשים בפקודה הבאה:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
לחלופין, אם התקנתם את הכלי GitHub's cli, אתם יכולים להשתמש בפקודה הבאה:
gh repo clone flutter/codelabs flutter-codelabs
הקוד לדוגמה משוכפל לספרייה flutter-codelabs
שמכילה את הקוד של אוסף של סדנאות קוד. הקוד של ה-Codelab הזה נמצא ב-flutter-codelabs/in_app_purchases
.
מבנה הספריות מתחת ל-flutter-codelabs/in_app_purchases
מכיל סדרה של תמונות מצב שמראות איפה אתם אמורים להיות בסוף כל שלב. קוד ההתחלה נמצא בשלב 0, ולכן צריך לנווט אליו באופן הבא:
cd flutter-codelabs/in_app_purchases/step_00
אם רוצים לדלג קדימה או לראות איך משהו אמור להיראות אחרי שלב מסוים, אפשר לעיין בספרייה שנקראת על שם השלב שמעניין אתכם. הקוד של השלב האחרון נמצא בתיקייה complete
.
הגדרת פרויקט התחלתי
פותחים את פרויקט המתחילים מ-step_00/app
בסביבת הפיתוח המשולבת (IDE) המועדפת. השתמשנו ב-Android Studio לצילומי המסך, אבל Visual Studio Code היא גם אפשרות מצוינת. בכל אחד מהעורכים, מוודאים שהתוספים העדכניים של Dart ו-Flutter מותקנים.
האפליקציות שאתם יוצרים צריכות לתקשר עם App Store ועם Play Store כדי לדעת אילו מוצרים זמינים ובאיזה מחיר. כל אפליקציה מזוהה באמצעות מזהה ייחודי. ב-App Store ל-iOS זה נקרא מזהה החבילה, וב-Play Store ל-Android זה נקרא מזהה האפליקציה. בדרך כלל, המזהים האלה נוצרים באמצעות סימון של שם דומיין הפוך. לדוגמה, כשיוצרים אפליקציה לרכישה מתוך האפליקציה עבור flutter.dev, משתמשים ב-dev.flutter.inapppurchase
. תחשבו על מזהה לאפליקציה שלכם, ועכשיו תגדירו אותו בהגדרות הפרויקט.
קודם כול מגדירים את מזהה החבילה ב-iOS. כדי לעשות את זה, פותחים את הקובץ Runner.xcworkspace
באפליקציית Xcode.
במבנה התיקיות של Xcode, פרויקט Runner נמצא בחלק העליון, והיעדים Flutter, Runner וProducts נמצאים מתחת לפרויקט Runner. לוחצים לחיצה כפולה על Runner כדי לערוך את הגדרות הפרויקט, ואז לוחצים על Signing & Capabilities (חתימה ויכולות). מזינים את מזהה החבילה שבחרתם בשדה צוות כדי להגדיר את הצוות.
עכשיו אפשר לסגור את Xcode ולחזור אל Android Studio כדי לסיים את ההגדרה ל-Android. כדי לעשות זאת, פותחים את הקובץ build.gradle.kts
בקטע android/app,
ומשנים את applicationId
(בשורה 24 בצילום המסך שלמטה) למזהה האפליקציה, זהה למזהה החבילה של iOS. שימו לב: המזהים של חנויות iOS ו-Android לא חייבים להיות זהים, אבל אם הם זהים יש פחות סיכוי לטעות. לכן, במעבדת הקוד הזו נשתמש גם במזהים זהים.
3. התקנת הפלאגין
בחלק הזה של ה-codelab תתקינו את הפלאגין in_app_purchase.
הוספת יחסי תלות בקובץ pubspec
מוסיפים את in_app_purchase
ל-pubspec על ידי הוספת in_app_purchase
לתלות בפרויקט:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) flutter_lints 5.0.0 (6.0.0 available) + in_app_purchase 3.2.3 + in_app_purchase_android 0.4.0+3 + in_app_purchase_platform_interface 1.4.0 + in_app_purchase_storekit 0.4.4 + json_annotation 4.9.0 lints 5.1.1 (6.0.0 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) provider 6.1.5 (6.1.5+1 available) test_api 0.7.6 (0.7.7 available) Changed 5 dependencies! 7 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
פותחים את pubspec.yaml
ומוודאים ש-in_app_purchase
מופיע כערך בקטע dependencies
, ו-in_app_purchase_platform_interface
מופיע בקטע dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^6.0.0
cupertino_icons: ^1.0.8
firebase_auth: ^6.0.1
firebase_core: ^4.0.0
google_sign_in: ^7.1.1
http: ^1.5.0
intl: ^0.20.2
provider: ^6.1.5
logging: ^1.3.0
in_app_purchase: ^3.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
4. הגדרה של App Store
כדי להגדיר רכישות מתוך האפליקציה ולבדוק אותן ב-iOS, צריך ליצור אפליקציה חדשה ב-App Store וליצור בה מוצרים לרכישה. לא צריך לפרסם שום דבר או לשלוח את האפליקציה ל-Apple לבדיקה. כדי לעשות את זה, צריך חשבון פיתוח. אם אין לכם חשבון כזה, אתם צריכים להירשם לתוכנית המפתחים של אפל.
הסכמים בנושא אפליקציות בתשלום
כדי להשתמש ברכישות מתוך האפליקציה, צריך גם הסכם פעיל לגבי אפליקציות בתשלום ב-App Store Connect. עוברים לכתובת https://appstoreconnect.apple.com/ ולוחצים על Agreements, Tax, and Banking (הסכמים, מיסים ובנקאות).
כאן יופיעו הסכמים לאפליקציות בחינם ולאפליקציות בתשלום. הסטטוס של אפליקציות חינמיות צריך להיות פעיל, והסטטוס של אפליקציות בתשלום הוא חדש. חשוב לקרוא את התנאים, לאשר אותם ולהזין את כל המידע הנדרש.
אם הכול מוגדר בצורה נכונה, הסטטוס של אפליקציות בתשלום יהיה פעיל. זה חשוב מאוד כי לא תוכלו לנסות רכישות מתוך האפליקציה בלי הסכם פעיל.
רישום מזהה האפליקציה
יוצרים מזהה חדש בפורטל Apple Developer. נכנסים לכתובת developer.apple.com/account/resources/identifiers/list ולוחצים על סמל הפלוס לצד הכותרת מזהים.
בחירת מזהי אפליקציות
בחירת אפליקציה
מוסיפים תיאור ומגדירים את מזהה החבילה כך שיהיה זהה לערך שהוגדר קודם ב-XCode.
הוראות נוספות ליצירת מזהה אפליקציה חדש זמינות במרכז העזרה לחשבון פיתוח.
יצירת אפליקציה חדשה
יוצרים אפליקציה חדשה ב-App Store Connect עם מזהה החבילה הייחודי שלכם.
למידע נוסף על יצירת אפליקציה חדשה וניהול הסכמים, אפשר לעיין בעזרה של App Store Connect.
כדי לבדוק את הרכישות מתוך האפליקציה, צריך משתמש ארגז חול לבדיקה. המשתמש הבודק הזה לא צריך להיות מחובר ל-iTunes – הוא משמש רק לבדיקת רכישות מתוך האפליקציה. אי אפשר להשתמש בכתובת אימייל שכבר משמשת לחשבון אפל. בקטע משתמשים וגישה, עוברים אל Sandbox כדי ליצור חשבון חדש ב-Sandbox או כדי לנהל את מזהי Apple הקיימים ב-Sandbox.
עכשיו אפשר להגדיר את משתמש הסביבה הווירטואלית באייפון. לשם כך, עוברים אל הגדרות > מפתח > חשבון סביבת בדיקה של אפל.
הגדרת רכישות מתוך האפליקציה
עכשיו מגדירים את שלושת הפריטים שניתן לרכוש:
-
dash_consumable_2k
: רכישה מתכלה שאפשר לרכוש הרבה פעמים, שמעניקה למשתמש 2,000 יחידות Dash (המטבע באפליקציה) לכל רכישה. -
dash_upgrade_3d
: רכישה של 'שדרוג' חד-פעמי שלא ניתן לצרוך, שמעניקה למשתמש אפשרות ללחוץ על מקף שונה מבחינה ויזואלית. -
dash_subscription_doubler
: מינוי שמעניק למשתמש פי שניים דאשים לכל קליק למשך תקופת המינוי.
עוברים אל רכישות מתוך האפליקציה.
יוצרים את הרכישות מתוך האפליקציה עם המזהים שצוינו:
- הגדרה של
dash_consumable_2k
כפריט מתכלה. משתמשים ב-dash_consumable_2k
כמזהה המוצר. השם לצורך הפניה משמש רק ב-App Store Connect, לכן פשוט מגדירים אותו ל-dash consumable 2k
.הגדרת הזמינות. המוצר צריך להיות זמין במדינה של משתמש ארגז החול.
מוסיפים מחיר ומגדירים אותו ל-
$1.99
או לסכום שווה ערך במטבע אחר.מוסיפים את הלוקליזציות לרכישה. קוראים לרכישה
Spring is in the air
עם2000 dashes fly out
כתיאור.מוסיפים צילום מסך של הביקורת. התוכן לא משנה אלא אם המוצר נשלח לבדיקה, אבל הוא נדרש כדי שהמוצר יהיה במצב 'מוכן לשליחה', שנדרש כשהאפליקציה מאחזרת מוצרים מ-App Store.
- הגדרת
dash_upgrade_3d
כפריט שלא ניתן לצריכה. משתמשים ב-dash_upgrade_3d
כמזהה המוצר. מגדירים את שם ההפניה ל-dash upgrade 3d
. קוראים לרכישה3D Dash
עםBrings your dash back to the future
כתיאור. הגדרת המחיר ל-$0.99
. מגדירים את הזמינות ומעלים את צילום המסך של הביקורת באותו אופן כמו עבור המוצרdash_consumable_2k
. - הגדרת
dash_subscription_doubler
כמינוי שמתחדש אוטומטית. תהליך ההרשמה למינויים קצת שונה. קודם צריך ליצור קבוצת מינויים. אם כמה מינויים הם חלק מאותה קבוצה, משתמש יכול להירשם רק לאחד מהם בכל פעם, אבל הוא יכול לשדרג או לשנמך בין המינויים האלה. פשוט להתקשר לקבוצהsubscriptions
.מוסיפים לוקליזציה לקבוצת המנויים.
בשלב הבא יוצרים את המינוי. מגדירים את שם ההפניה ל-
dash subscription doubler
ואת מזהה המוצר ל-dash_subscription_doubler
.בשלב הבא, בוחרים את משך המינוי (שבוע אחד) ואת הלוקליזציות. תקרא למינוי הזה
Jet Engine
ותוסיף את התיאורDoubles your clicks
. הגדרת המחיר ל-$0.49
. מגדירים את הזמינות ומעלים את צילום המסך של הביקורת באותו אופן כמו עבור המוצרdash_consumable_2k
.
עכשיו המוצרים אמורים להופיע ברשימות:
5. הגדרת חנות Play
כמו בחנות האפליקציות, תצטרכו גם חשבון מפתח בחנות Play. אם עדיין אין לכם חשבון, צריך להירשם.
יצירת אפליקציה חדשה
יוצרים אפליקציה חדשה ב-Google Play Console:
- פותחים את Play Console.
- בוחרים באפשרות כל האפליקציות > יצירת אפליקציה.
- בוחרים שפת ברירת מחדל ומוסיפים שם לאפליקציה. מקלידים את שם האפליקציה כפי שרוצים שהוא יופיע ב-Google Play. אפשר לשנות את השם בהמשך.
- מציינים שהאפליקציה היא משחק. אפשר לשנות את הבחירה בשלב מאוחר יותר.
- מציינים אם האפליקציה חינמית או בתשלום.
- משלימים את ההצהרות בנושא הנחיות לתוכן וחוקי הייצוא של ארה"ב.
- לוחצים על יצירת אפליקציה.
אחרי שיוצרים את האפליקציה, עוברים ללוח הבקרה ומשלימים את כל המשימות בקטע הגדרת האפליקציה. כאן צריך לספק מידע על האפליקציה, כמו סיווג תוכן וצילומי מסך.
חתימה על הבקשה
כדי לבדוק רכישות מתוך האפליקציה, צריך להעלות לפחות גרסת build אחת ל-Google Play.
לשם כך, צריך לחתום על גרסת ה-release באמצעות משהו אחר מלבד מפתחות הניפוי באגים.
יצירת מאגר מפתחות
אם יש לכם מאגר מפתחות קיים, מדלגים לשלב הבא. אם לא, יוצרים אותו על ידי הרצת הפקודה הבאה בשורת הפקודה.
ב-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; don't check it into public source control!
הפניה אל מאגר המפתחות מהאפליקציה
יוצרים קובץ בשם <your app dir>/android/key.properties
שמכיל הפניה למאגר המפתחות:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
הגדרת חתימה ב-Gradle
עורכים את קובץ <your app dir>/android/app/build.gradle.kts
כדי להגדיר חתימה לאפליקציה.
מוסיפים את פרטי מאגר המפתחות מקובץ המאפיינים לפני הבלוק android
:
import java.util.Properties
import java.io.FileInputStream
plugins {
// omitted
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
טוענים את הקובץ key.properties
לאובייקט keystoreProperties
.
מעדכנים את הבלוק buildTypes
כך:
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
מגדירים את הבלוק signingConfigs
בקובץ build.gradle.kts
של המודול עם פרטי הגדרות החתימה:
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
מעכשיו, חתימה על גרסאות של האפליקציה תתבצע באופן אוטומטי.
מידע נוסף על חתימת האפליקציה זמין במאמר חתימת האפליקציה באתר developer.android.com.
העלאת הגרסה הראשונה
אחרי שמגדירים את האפליקציה לחתימה, אפשר ליצור את האפליקציה על ידי הרצת הפקודה:
flutter build appbundle
הפקודה הזו יוצרת כברירת מחדל גרסת build של גרסה, והפלט נמצא ב-<your app dir>/build/app/outputs/bundle/release/
מלוח הבקרה ב-Google Play Console, עוברים אל בדיקה והשקה > בדיקה > בדיקה בקבוצה מוגדרת ויוצרים גרסת בדיקה חדשה בקבוצה מוגדרת.
לאחר מכן, מעלים את חבילת האפליקציות app-release.aab
שנוצרה על ידי פקודת הבנייה.
לוחצים על שמירה ואז על בדיקת הגרסה.
לבסוף, לוחצים על השקת הגרסה לבדיקות בקבוצות מוגדרות כדי להפעיל את הגרסה לבדיקות בקבוצות מוגדרות.
הגדרת משתמשי בדיקה
כדי לבדוק רכישות מתוך האפליקציה, צריך להוסיף את חשבונות Google של הבודקים בשני מקומות ב-Google Play Console:
- למסלול הבדיקה הספציפי (בדיקה פנימית)
- כבודקי רישיונות
קודם כול, מוסיפים את הבודק למסלול הבדיקה הפנימית. חוזרים אל בדיקה ופרסום > בדיקה > בדיקה פנימית ולוחצים על הכרטיסייה בודקים.
כדי ליצור רשימה חדשה של כתובות אימייל, לוחצים על יצירת רשימת כתובות אימייל. נותנים לרשימה שם ומוסיפים את כתובות האימייל של חשבונות Google שצריכים לקבל גישה לבדיקת רכישות מתוך האפליקציה.
לאחר מכן, מסמנים את תיבת הסימון שלצד הרשימה ולוחצים על שמירת השינויים.
לאחר מכן, מוסיפים את בודקי הרישיונות:
- חוזרים לתצוגה כל האפליקציות ב-Google Play Console.
- עוברים אל הגדרות > בדיקת רישיון.
- מוסיפים את אותן כתובות אימייל של הבודקים שצריכים לבדוק רכישות מתוך האפליקציה.
- מגדירים את License response (תגובה לרישיון) ל-
RESPOND_NORMALLY
. - לוחצים על שמירת השינויים.
הגדרת רכישות מתוך האפליקציה
עכשיו מגדירים את הפריטים שאפשר לקנות באפליקציה.
בדיוק כמו ב-App Store, אתם צריכים להגדיר שלוש רכישות שונות:
-
dash_consumable_2k
: רכישה מתכלה שאפשר לרכוש הרבה פעמים, שמעניקה למשתמש 2,000 יחידות Dash (המטבע באפליקציה) לכל רכישה. -
dash_upgrade_3d
: רכישת 'שדרוג' חד-פעמית שלא ניתן לצרוך, שמעניקה למשתמש מקף שונה מבחינה ויזואלית שאפשר ללחוץ עליו. -
dash_subscription_doubler
: מינוי שמעניק למשתמש פי שניים דאשים לכל קליק למשך תקופת המינוי.
קודם כול, מוסיפים את הפריט המתכלה ואת הפריט הלא מתכלה.
- עוברים אל Google Play Console ובוחרים את האפליקציה.
- עוברים אל מונטיזציה > מוצרים > מוצרים מתוך האפליקציה.
- לוחצים על יצירת מוצר
.
- מזינים את כל המידע שנדרש לגבי המוצר. חשוב לוודא שמזהה המוצר תואם בדיוק למזהה שבו אתם רוצים להשתמש.
- לוחצים על שמירה.
- לוחצים על הפעלה.
- חוזרים על התהליך עבור הרכישה של 'שדרוג' שאי אפשר לצרוך.
בשלב הבא, מוסיפים את המינוי:
- עוברים אל Google Play Console ובוחרים את האפליקציה.
- עוברים אל מונטיזציה > מוצרים > מינויים.
- לוחצים על יצירת מינוי
.
- מזינים את כל המידע שנדרש לגבי המינוי. חשוב לוודא שמזהה המוצר זהה בדיוק למזהה שבו אתם רוצים להשתמש.
- לוחצים על שמירה.
ההגדרות של הרכישות שלכם אמורות להופיע עכשיו ב-Play Console.
6. הגדרת Firebase
ב-codelab הזה תשתמשו בשירות בק-אנד כדי לאמת ולעקוב אחרי הרכישות של המשתמשים.
לשימוש בשירות backend יש כמה יתרונות:
- אתם יכולים לאמת עסקאות בצורה מאובטחת.
- אתם יכולים להגיב לאירועי חיוב מחנויות האפליקציות.
- אפשר לעקוב אחרי הרכישות במסד נתונים.
- המשתמשים לא יוכלו להערים על האפליקציה כדי לקבל תכונות פרימיום על ידי החזרת השעון של המערכת לאחור.
יש הרבה דרכים להגדיר שירות קצה עורפי, אבל אנחנו נשתמש ב-Cloud Functions וב-Firestore באמצעות Firebase של Google.
כתיבת הקצה העורפי לא נכללת בתחום של ה-codelab הזה, ולכן קוד ההתחלה כבר כולל פרויקט Firebase שמטפל ברכישות בסיסיות כדי שתוכלו להתחיל.
גם פלאגינים של Firebase כלולים באפליקציית המתחילים.
מה שנותר לכם לעשות הוא ליצור פרויקט Firebase משלכם, להגדיר את האפליקציה ואת ה-Backend ל-Firebase, ולבסוף לפרוס את ה-Backend.
יצירת פרויקט 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, עוברים אל Project Overview (סקירת הפרויקט), בוחרים באפשרות Settings (הגדרות) ולוחצים על הכרטיסייה General (כללי).
גוללים למטה אל האפליקציות שלך ובוחרים באפליקציה dashclicker (android).
כדי לאפשר כניסה באמצעות חשבון Google במצב ניפוי באגים, צריך לספק את טביעת האצבע של הגיבוב SHA-1 של אישור ניפוי הבאגים.
קבלת הגיבוב (hash) של אישור החתימה לניפוי באגים
בשורש של פרויקט אפליקציית Flutter, משנים את הספרייה לתיקייה android/
ואז יוצרים דוח חתימה.
cd android ./gradlew :app:signingReport
תוצג רשימה ארוכה של מפתחות חתימה. מכיוון שאתם מחפשים את הגיבוב של אישור הניפוי באגים, חפשו את האישור עם המאפיינים Variant
ו-Config
שהערך שלהם הוא debug
. סביר להניח שמאגר המפתחות נמצא בתיקיית הבית שלכם בנתיב .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
מעתיקים את הגיבוב SHA-1 וממלאים את השדה האחרון בתיבת הדו-שיח של שליחת האפליקציה.
בסוף, מריצים שוב את הפקודה flutterfire configure
כדי לעדכן את האפליקציה כך שתכלול את הגדרות החתימה.
$ flutterfire configure ? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes ✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
הגדרה של Firebase ל-iOS: שלבים נוספים
פותחים את ios/Runner.xcworkspace
באמצעות Xcode
. או באמצעות סביבת הפיתוח המשולבת (IDE) שתבחרו.
ב-VSCode לוחצים לחיצה ימנית על התיקייה ios/
ואז על open in xcode
.
ב-Android Studio, לוחצים לחיצה ימנית על התיקייה ios/
, לוחצים על flutter
ואז על האפשרות open iOS module in Xcode
.
כדי לאפשר כניסה באמצעות חשבון Google ב-iOS, מוסיפים את אפשרות ההגדרה CFBundleURLTypes
לקובצי ה-build plist
. (מידע נוסף זמין במסמכי התיעוד של google_sign_in
החבילה). במקרה הזה, הקובץ הוא ios/Runner/Info.plist
.
צמד המפתח/ערך כבר נוסף, אבל צריך להחליף את הערכים שלו:
- מקבלים את הערך של
REVERSED_CLIENT_ID
מהקובץGoogleService-Info.plist
, בלי הרכיב<string>..</string>
שמקיף אותו. - מחליפים את הערך בקובץ
ios/Runner/Info.plist
במפתחCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
סיימתם את ההגדרה של Firebase.
7. האזנה לעדכונים על רכישות
בחלק הזה של ה-codelab תכינו את האפליקציה לרכישת המוצרים. התהליך הזה כולל האזנה לעדכונים ולשגיאות ברכישות אחרי שהאפליקציה מופעלת.
האזנה לעדכונים על רכישות
ב-main.dart,
מוצאים את הווידג'ט MyHomePage
עם Scaffold
שמכיל BottomNavigationBar
עם שני דפים. בדף הזה נוצרים גם שלושה Provider
עבור DashCounter
, DashUpgrades,
וDashPurchases
. DashCounter
עוקב אחרי המספר הנוכחי של המקפים ומגדיל אותו אוטומטית. DashUpgrades
מנהל את השדרוגים שאפשר לקנות באמצעות Dash. ה-codelab הזה מתמקד ב-DashPurchases
.
כברירת מחדל, האובייקט של ספק מוגדר כשהאובייקט הזה מתבקש בפעם הראשונה. האובייקט הזה מאזין לעדכוני רכישות ישירות כשהאפליקציה מופעלת, לכן צריך להשבית את הטעינה העצלנית באובייקט הזה באמצעות lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
צריך גם מופע של InAppPurchaseConnection
. עם זאת, כדי שאפשר יהיה לבדוק את האפליקציה, צריך למצוא דרך לדמות את החיבור. כדי לעשות את זה, יוצרים method של מופע שאפשר לבטל בבדיקה, ומוסיפים אותו ל-main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
מעדכנים את הבדיקה באופן הבא:
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable({
required PurchaseParam purchaseParam,
bool autoConsume = true,
}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(
ProductDetailsResponse(productDetails: [], notFoundIDs: []),
);
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
ב-lib/logic/dash_purchases.dart
, עוברים לקוד של DashPurchasesChangeNotifier
. בשלב הזה, יש רק DashCounter
שאפשר להוסיף ללוחות הבקרה שרכשתם.
מוסיפים מאפיין של מינוי לסטרימינג, _subscription
(מסוג StreamSubscription<List<PurchaseDetails>> _subscription;
), את IAPConnection.instance,
ואת הייבוא. הקוד שיתקבל צריך להיראות כך:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
מילת המפתח late
נוספת ל-_subscription
כי _subscription
מאותחל בבונה. הפרויקט הזה מוגדר כברירת מחדל כלא ניתן לאכלוס (NNBD), כלומר מאפיינים שלא הוגדרו כניתנים לאכלוס חייבים להכיל ערך שאינו null. התוחם late
מאפשר להגדיר את הערך הזה מאוחר יותר.
ב-constructor, מקבלים את הזרם purchaseUpdated
ומתחילים להאזין לו. בשיטה dispose()
, מבטלים את המינוי לשידורים.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
עכשיו האפליקציה מקבלת את עדכוני הרכישה, אז בקטע הבא תבצעו רכישה.
לפני שממשיכים, מריצים את הבדיקות עם flutter test"
כדי לוודא שהכול מוגדר בצורה נכונה.
$ flutter test 00:01 +1: All tests passed!
8. לבצע רכישות
בחלק הזה של ה-codelab, תחליפו את המוצרים הקיימים לניסיון במוצרים אמיתיים שאפשר לקנות. המוצרים האלה נטענים מהחנויות, מוצגים ברשימה ונרכשים כשמקישים על המוצר.
Adapt PurchasableProduct
PurchasableProduct
מוצג מוצר לדוגמה. כדי לעדכן את הקוד כך שיוצג בו תוכן אמיתי, מחליפים את המחלקה PurchasableProduct
ב-purchasable_product.dart
בקוד הבא:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus { purchasable, purchased, pending }
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
בשלב dash_purchases.dart,
מסירים את הרכישות הפיקטיביות ומחליפים אותן ברשימה ריקה, List<PurchasableProduct> products = [];
.
טעינת רכישות זמינות
כדי לתת למשתמש אפשרות לבצע רכישה, צריך לטעון את הרכישות מהחנות. קודם כל, בודקים אם החנות זמינה. אם החנות לא זמינה, הגדרת הערך storeState
ל-notAvailable
תציג הודעת שגיאה למשתמש.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
כשהחנות זמינה, טוענים את הרכישות הזמינות. בהתאם להגדרות הקודמות של Google Play ו-App Store, אמורים להופיע storeKeyConsumable
, storeKeySubscription,
ו-storeKeyUpgrade
. אם רכישה צפויה לא זמינה, המידע הזה יודפס במסוף. כדאי גם לשלוח את המידע הזה לשירות העורפי.
השיטה await iapConnection.queryProductDetails(ids)
מחזירה גם את המזהים שלא נמצאו וגם את המוצרים שניתן לרכוש שנמצאו. משתמשים ב-productDetails
מהתגובה כדי לעדכן את ממשק המשתמש, ומגדירים את StoreState
ל-available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
// ...
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails
.map((e) => PurchasableProduct(e))
.toList();
storeState = StoreState.available;
notifyListeners();
}
מפעילים את הפונקציה loadPurchases()
ב-constructor:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases(); // Add this line
}
לבסוף, משנים את הערך של השדה storeState
מ-StoreState.available
ל-StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
הצגת מוצרים שאפשר לקנות
בודקים את קובץ ה-purchase_page.dart
. בווידג'ט PurchasePage
מוצגים הערכים _PurchasesLoading
, _PurchaseList,
או _PurchasesNotAvailable,
בהתאם לStoreState
. בווידג'ט מוצגות גם הרכישות הקודמות של המשתמש, שמשמשות בשלב הבא.
ווידג'ט _PurchaseList
מציג את רשימת המוצרים שאפשר לקנות ושולח בקשת קנייה לאובייקט DashPurchases
.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map(
(product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
},
),
)
.toList(),
);
}
}
אם ההגדרה בוצעה בצורה נכונה, המוצרים הזמינים אמורים להופיע בחנויות של Android ו-iOS. שימו לב: יכול להיות שיעבור זמן מה עד שהרכישות יהיו זמינות אחרי שתזינו אותן במסופים המתאימים.
חוזרים אל dash_purchases.dart
ומטמיעים את הפונקציה לקניית מוצר. צריך להפריד רק בין מוצרים מתכלים לבין מוצרים לא מתכלים. השדרוג והמוצרים במינוי הם מוצרים לא מתכלים.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails,
'${product.id} is not a known product',
);
}
}
לפני שממשיכים, יוצרים את המשתנה _beautifiedDashUpgrade
ומעדכנים את הפונקציה beautifiedDash
getter כדי להפנות אליו.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
השיטה _onPurchaseUpdate
מקבלת את עדכוני הרכישה, מעדכנת את הסטטוס של המוצר שמוצג בדף הרכישה ומחיל את הרכישה על לוגיקת המונה. חשוב לקרוא ל-completePurchase
אחרי הטיפול ברכישה, כדי שהחנות תדע שהרכישה טופלה בצורה נכונה.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. הגדרת הקצה העורפי
לפני שמתקדמים למעקב אחרי רכישות ולאימות שלהן, צריך להגדיר קצה עורפי של Dart כדי לתמוך בפעולות האלה.
בקטע הזה, עובדים מהתיקייה dart-backend/
כשורש.
ודאו שהכלים הבאים מותקנים:
- Dart
- Firebase CLI
סקירה כללית של פרויקט בסיסי
חלקים מסוימים בפרויקט הזה נחשבים מחוץ להיקף של ה-Codelab הזה, ולכן הם כלולים בקוד ההתחלתי. לפני שמתחילים, כדאי לעבור על מה שכבר קיים בקוד ההתחלתי כדי להבין איך כדאי לתכנן את הדברים.
קוד ה-Backend הזה יכול לפעול באופן מקומי במחשב שלכם, ולא צריך לפרוס אותו כדי להשתמש בו. עם זאת, אתם צריכים להיות מסוגלים להתחבר ממכשיר הפיתוח (Android או iPhone) למכונה שבה השרת יפעל. לשם כך, הם צריכים להיות באותה רשת, ואתם צריכים לדעת את כתובת ה-IP של המחשב שלכם.
מנסים להריץ את השרת באמצעות הפקודה הבאה:
$ dart ./bin/server.dart Serving at http://0.0.0.0:8080
הקצה העורפי של Dart משתמש ב-shelf
וב-shelf_router
כדי להציג נקודות קצה של API. כברירת מחדל, השרת לא מספק נתיבים. בהמשך תיצרו מסלול לטיפול בתהליך אימות הרכישה.
חלק אחד שכבר כלול בקוד ההתחלתי הוא IapRepository
ב-lib/iap_repository.dart
. ההסבר על האינטראקציה עם Firestore או עם מסדי נתונים באופן כללי לא רלוונטי ל-codelab הזה, ולכן קוד ההתחלה מכיל פונקציות ליצירה או לעדכון של רכישות ב-Firestore, וגם את כל המחלקות של הרכישות האלה.
הגדרת גישה ל-Firebase
כדי לגשת ל-Firebase Firestore, צריך מפתח גישה לחשבון שירות. כדי ליצור מפתח פרטי, פותחים את ההגדרות של פרויקט Firebase, עוברים לקטע חשבונות שירות ובוחרים באפשרות יצירת מפתח פרטי חדש.
מעתיקים את קובץ ה-JSON שהורדתם לתיקייה assets/
ומשנים את השם שלו ל-service-account-firebase.json
.
הגדרת גישה ל-Google Play
כדי לגשת לחנות Play ולאמת רכישות, צריך ליצור חשבון שירות עם ההרשאות האלה ולהוריד את פרטי הכניסה שלו בפורמט JSON.
- נכנסים לדף Google Play Android Developer API במסוף Google Cloud.
אם ב-Google Play Console מתבקשים ליצור פרויקט או לקשר לפרויקט קיים, צריך לעשות זאת קודם ואז לחזור לדף הזה.
- עוברים אל הדף 'חשבונות שירות' ולוחצים על + יצירת חשבון שירות.
- מזינים את שם חשבון השירות ולוחצים על יצירה והמשך.
- בוחרים בתפקיד Pub/Sub Subscriber ולוחצים על Done.
- אחרי שיוצרים את החשבון, עוברים אל ניהול מפתחות.
- בוחרים באפשרות Add key > Create new key (הוספת מפתח > יצירת מפתח חדש).
- יוצרים ומורידים מפתח JSON.
- משנים את השם של הקובץ שהורדתם ל-
service-account-google-play.json,
ומעבירים אותו לספרייהassets/
. - לאחר מכן, עוברים לדף משתמשים והרשאות ב-Play Console
- לוחצים על הזמנת משתמשים חדשים ומזינים את כתובת האימייל של חשבון השירות שנוצר קודם. אפשר למצוא את כתובת האימייל בטבלה בדף Service accounts
- נותנים לאפליקציה את ההרשאות הצגת נתונים פיננסיים וניהול הזמנות ומינויים.
- לוחצים על הזמנת משתמש.
עוד דבר שצריך לעשות הוא לפתוח את 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.
בשתי החנויות, האפליקציה מקבלת אסימון כשמתבצעת רכישה.
האפליקציה שולחת את האסימון הזה לשירות הבק-אנד שלכם, שמאמת את הרכישה מול השרתים של החנות הרלוונטית באמצעות האסימון שסופק.
לאחר מכן, שירות ה-Backend יכול לבחור אם לאחסן את הרכישה, ולהשיב לאפליקציה אם הרכישה תקפה או לא.
אם שירות ה-backend מבצע את האימות מול החנויות במקום האפליקציה שפועלת במכשיר של המשתמש, אפשר למנוע מהמשתמש לקבל גישה לתכונות פרימיום, למשל על ידי החזרת השעון של המערכת אחורה.
הגדרת הצד של Flutter
הגדרת אימות
אתם מתכוונים לשלוח את הרכישות לשירות הקצה העורפי, ולכן אתם רוצים לוודא שהמשתמש מאומת בזמן הרכישה. רוב הלוגיקה של האימות כבר נוספה בשבילכם בפרויקט המתחיל, וכל מה שאתם צריכים לעשות זה לוודא שPurchasePage
מציג את לחצן הכניסה כשהמשתמש עדיין לא מחובר. מוסיפים את הקוד הבא לתחילת שיטת ה-build של PurchasePage
:
lib/pages/purchase_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart'; // Add this import
import '../model/firebase_state.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart'; // And this one as well
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) { // Update from here
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
} // To here.
// ...
התקשרות לנקודת הקצה של אימות השיחות מהאפליקציה
באפליקציה, יוצרים את הפונקציה _verifyPurchase(PurchaseDetails purchaseDetails)
שקוראת לנקודת הקצה /verifypurchase
בשרת הקצה העורפי של Dart באמצעות קריאת http post.
שולחים את החנות שנבחרה (google_play
לחנות Play או app_store
ל-App Store), את serverVerificationData
ואת productID
. השרת מחזיר קוד סטטוס שמציין אם הרכישה אומתה.
בקבועי האפליקציה, מגדירים את כתובת ה-IP של השרת לכתובת ה-IP של המחשב המקומי.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'dart:convert'; // Add this import
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // And this import
import 'package:in_app_purchase/in_app_purchase.dart';
import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart'; // And this one
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier; // Add this line
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter, this.firebaseNotifier) { // Update this line
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
הוספת firebaseNotifier
עם יצירת DashPurchases
ב-main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
מוסיפים פונקציית getter למשתמש ב-FirebaseNotifier, כדי שאפשר יהיה להעביר את מזהה המשתמש לפונקציית אימות הרכישה.
lib/logic/firebase_notifier.dart
Future<FirebaseFirestore> get firestore async {
var isInitialized = await _isInitialized.future;
if (!isInitialized) {
throw Exception('Firebase is not initialized');
}
return FirebaseFirestore.instance;
}
User? get user => FirebaseAuth.instance.currentUser; // Add this line
Future<void> load() async {
// ...
מוסיפים את הפונקציה _verifyPurchase
לכיתה DashPurchases
. הפונקציה async
מחזירה ערך בוליאני שמציין אם הרכישה אומתה.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
מתקשרים לפונקציה _verifyPurchase
ב-_handlePurchase
ממש לפני שמחילים את הרכישה. צריך להחיל את הרכישה רק אחרי שהיא מאומתת. באפליקציה שנמצאת בסביבת הייצור, אפשר לציין את זה בצורה מפורטת יותר, למשל כדי להחיל מינוי לתקופת ניסיון כשהחנות לא זמינה באופן זמני. עם זאת, בדוגמה הזו, נחיל את הרכישה כשהיא תאומת בהצלחה.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
באפליקציה, הכול מוכן עכשיו לאימות הרכישות.
הגדרת שירות הקצה העורפי
לאחר מכן, מגדירים את ה-backend לאימות רכישות ב-backend.
יצירת רכיבי handler של רכישות
תהליך האימות בשתי החנויות כמעט זהה, ולכן כדאי להגדיר מחלקה מופשטת 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:
הטוקן שסופק למשתמש על ידי החנות.
בנוסף, כדי להקל על השימוש ב-handlers האלה של רכישות, מוסיפים שיטה 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,
),
};
לאחר מכן, מגדירים כמה הטמעות של placeholder לחנות 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
. ה-handler של הרכישה משתמש במופע הזה כדי לאחסן מידע על רכישות ב-Firestore בהמשך. כדי לתקשר עם Google Play, משתמשים בAndroidPublisherApi
שסופק.
לאחר מכן, מבצעים את אותה פעולה עבור רכיב ה-handler של חנות האפליקציות. יוצרים lib/app_store_purchase_handler.dart
ומוסיפים שוב כיתה שמרחיבה את PurchaseHandler
:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(this.iapRepository);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
מצוין! עכשיו יש לכם שני רכיבי handler של רכישות. לאחר מכן, יוצרים את נקודת הקצה של ה-API לאימות הרכישה.
שימוש ב-Purchase Handlers
פותחים את bin/server.dart
ויוצרים נקודת קצה (endpoint) של API באמצעות shelf_route
:
bin/server.dart
import 'dart:convert';
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
if (payload case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
הקוד מבצע את הפעולות הבאות:
- מגדירים נקודת קצה של POST שתופעל מהאפליקציה שיצרתם קודם.
- מפענחים את מטען ה-JSON הייעודי ומחלצים את הפרטים הבאים:
-
userId
: מזהה המשתמש המחובר -
source
: החנות שבה נעשה שימוש,app_store
אוgoogle_play
. -
productData
: מתקבל מ-productDataMap
שיצרתם קודם. -
token
: מכיל את נתוני האימות שצריך לשלוח לחנויות.
-
- קוראים לפונקציה
verifyPurchase
, או ל-GooglePlayPurchaseHandler
או ל-AppStorePurchaseHandler
, בהתאם למקור. - אם האימות בוצע בהצלחה, הפונקציה מחזירה
Response.ok
ללקוח. - אם האימות נכשל, השיטה מחזירה
Response.internalServerError
ללקוח.
אחרי שיוצרים את נקודת הקצה של ה-API, צריך להגדיר את שני רכיבי ה-handler של הרכישה. לשם כך, צריך לטעון את המפתחות של חשבון השירות שהתקבלו בשלב הקודם ולהגדיר את הגישה לשירותים השונים, כולל Android Publisher API ו-Firebase Firestore API. לאחר מכן, יוצרים את שני רכיבי ה-handler של הרכישה עם התלויות השונות:
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
אימות רכישות ב-Android: הטמעה של רכיב לטיפול ברכישות
לאחר מכן, ממשיכים להטמיע את רכיב ה-handler של הרכישות ב-Google Play.
Google כבר מספקת חבילות Dart לאינטראקציה עם ממשקי ה-API שדרושים לאימות רכישות. הגדרתם אותם בקובץ server.dart
ועכשיו אתם משתמשים בהם במחלקה GooglePlayPurchaseHandler
.
הטמעה של רכישות שאינן מסוג מינוי:
lib/google_play_purchase_handler.dart
/// Handle non-subscription purchases (one time purchases).
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
אפשר לעדכן את רכיב ה-handler של רכישת המינוי באופן דומה:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
מוסיפים את השיטה הבאה כדי להקל על ניתוח מזהי ההזמנות, וגם שתי שיטות לניתוח סטטוס הרכישה.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
הרכישות שלכם ב-Google Play אמורות להיות מאומתות עכשיו ולשמור במסד הנתונים.
לאחר מכן, עוברים לרכישות מ-App Store ב-iOS.
אימות רכישות ב-iOS: הטמעה של רכיב לטיפול ברכישות
כדי לאמת רכישות ב-App Store, יש חבילת Dart של צד שלישי בשם app_store_server_sdk
שמקלה על התהליך.
קודם יוצרים את מופע ITunesApi
. כדאי להשתמש בהגדרות ארגז החול ולהפעיל רישום ביומן כדי לאתר באגים בשגיאות.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(ITunesEnvironment.sandbox(), loggingEnabled: true),
);
עכשיו, בניגוד ל-Google Play APIs, ב-App Store נעשה שימוש באותם נקודות קצה של API גם למינויים וגם למוצרים שאינם מינויים. המשמעות היא שאפשר להשתמש באותה לוגיקה בשני ה-handlers. ממזגים אותם כך שהם קוראים לאותו יישום:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
// See next step
}
עכשיו מטמיעים את handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(
NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0'),
),
type: product.type,
status: NonSubscriptionStatus.completed,
),
);
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(
SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0'),
),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0'),
),
status: SubscriptionStatus.active,
),
);
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
הרכישות שלכם ב-App Store אמורות להיות מאומתות עכשיו ומאוחסנות במסד הנתונים.
הפעלת ה-backend
בשלב הזה אתם יכולים להריץ את dart bin/server.dart
כדי להציג את נקודת הקצה /verifypurchase
.
$ dart bin/server.dart Serving at http://0.0.0.0:8080
11. מעקב אחר רכישות
הדרך המומלצת לעקוב אחרי הרכישות של המשתמשים היא בשירות העורפי. הסיבה לכך היא שהקצה העורפי יכול להגיב לאירועים מהחנות, ולכן הוא פחות מועד לשימוש במידע לא מעודכן בגלל שמירה במטמון, וגם פחות רגיש לשיבוש.
קודם כל, מגדירים את העיבוד של אירועים בחנות בקצה העורפי באמצעות קצה העורפי של Dart שאתם בונים.
עיבוד אירועים בחנות בקצה העורפי
חנויות יכולות להודיע לשרת העורפי על אירועי חיוב שמתרחשים, למשל כשמינויים מתחדשים. אתם יכולים לעבד את האירועים האלה בקצה העורפי כדי שהרכישות במסד הנתונים שלכם יהיו עדכניות. בקטע הזה, מגדירים את האפשרות הזו גם לחנות Google Play וגם ל-App Store של Apple.
עיבוד אירועי חיוב ב-Google Play
Google Play מספקת אירועי חיוב דרך מה שהם מכנים נושא Pub/Sub ב-Cloud. אלה בעצם תורים של הודעות שאפשר לפרסם בהם הודעות וגם לקרוא מהם הודעות.
מכיוון שהפונקציונליות הזו ספציפית ל-Google Play, צריך לכלול אותה ב-GooglePlayPurchaseHandler
.
כדי להתחיל, פותחים את lib/google_play_purchase_handler.dart
ומוסיפים את הייבוא של PubsubApi
:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
לאחר מכן מעבירים את PubsubApi
אל GooglePlayPurchaseHandler
ומשנים את בנאי המחלקה כדי ליצור Timer
באופן הבא:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
ה-Timer
מוגדר להפעיל את השיטה _pullMessageFromPubSub
כל עשר שניות. אפשר לשנות את משך הזמן בהתאם להעדפות שלכם.
לאחר מכן, יוצרים את _pullMessageFromPubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(maxMessages: 1000);
final topicName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(ackIds: [id]);
final subscriptionName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
הקוד שזה עתה הוספתם מתקשר עם נושא Pub/Sub מ-Google Cloud כל עשר שניות ומבקש הודעות חדשות. לאחר מכן, המערכת מעבדת כל הודעה בשיטה _processMessage
.
בשיטה הזו, ההודעות הנכנסות מפוענחות והמידע המעודכן על כל רכישה מתקבל, גם על מינויים וגם על רכישות חד-פעמיות. אם צריך, מתבצעת קריאה ל-handleSubscription
או ל-handleNonSubscription
.
צריך לאשר כל הודעה באמצעות השיטה _askMessage
.
לאחר מכן מוסיפים את יחסי התלות הנדרשים לקובץ server.dart
. מוסיפים את PubsubApi.cloudPlatformScope להגדרות פרטי הכניסה:
bin/server.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub; // Add this import
final clientGooglePlay = await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // Add this line
]);
לאחר מכן, יוצרים את המופע PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
ולבסוף, מעבירים אותו לבונה GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // Add this line
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
הגדרת Google Play
כתבתם את הקוד לצריכת אירועי חיוב מנושא ה-Pub/Sub, אבל לא יצרתם את נושא ה-Pub/Sub ולא פרסמתם אירועי חיוב. הגיע הזמן להגדיר את זה.
קודם כל, יוצרים נושא Pub/Sub:
- מגדירים את הערך של
googleCloudProjectId
ב-constants.dart
למזהה של הפרויקט ב-Google Cloud. - נכנסים לדף Cloud Pub/Sub במסוף Google Cloud.
- מוודאים שאתם נמצאים בפרויקט Firebase ולוחצים על + יצירת נושא.
- נותנים לנושא החדש שם זהה לערך שהוגדר ל-
googlePlayPubsubBillingTopic
ב-constants.dart
. במקרה הזה, נותנים את השםplay_billing
. אם בוחרים משהו אחר, צריך לעדכן אתconstants.dart
. יוצרים את הנושא. - ברשימת הנושאים שלכם ב-Pub/Sub, לוחצים על סמל שלוש הנקודות האנכיות של הנושא שיצרתם ולוחצים על View permissions (הצגת הרשאות).
- בסרגל הצד משמאל, לוחצים על הוספת ישות ראשית.
- מוסיפים כאן את
google-play-developer-notifications@system.gserviceaccount.com
ומעניקים לו את התפקיד פרסום הודעות ב-Pub/Sub. - שומרים את השינויים בהרשאות.
- מעתיקים את שם הנושא שיצרתם.
- פותחים שוב את Play Console ובוחרים את האפליקציה מהרשימה כל האפליקציות.
- גוללים למטה אל מונטיזציה > הגדרת מונטיזציה.
- ממלאים את הנושא המלא ושומרים את השינויים.
כל האירועים שקשורים לחיוב ב-Google Play יפורסמו עכשיו בנושא.
עיבוד אירועי חיוב בחנות האפליקציות
לאחר מכן, מבצעים את אותה פעולה לגבי אירועי חיוב ב-App Store. יש שתי דרכים יעילות להטמיע עדכונים בטיפול ברכישות ב-App Store. אחת מהן היא הטמעה של webhook שאתם מספקים ל-Apple, והיא משתמשת בו כדי לתקשר עם השרת שלכם. הדרך השנייה, שמוסברת ב-codelab הזה, היא להתחבר ל-App Store Server API ולקבל את פרטי המינוי באופן ידני.
הסיבה לכך ש-codelab הזה מתמקד בפתרון השני היא שצריך לחשוף את השרת לאינטרנט כדי להטמיע את ה-webhook.
בסביבת ייצור, מומלץ שיהיו לכם את שניהם. Webhook כדי לקבל אירועים מ-App Store, וממשק API של שרת למקרה שפספסתם אירוע או שאתם צריכים לבדוק שוב את סטטוס המינוי.
כדי להתחיל, פותחים את lib/app_store_purchase_handler.dart
ומוסיפים את התלות AppStoreServerAPI
:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI; // Add this member
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // And this parameter
);
משנים את ה-constructor כדי להוסיף טיימר שיבצע קריאה ל-method _pullStatus
. הטיימר הזה יפעיל את השיטה _pullStatus
כל 10 שניות. אפשר לשנות את משך הזמן של הטיימר בהתאם לצרכים.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
לאחר מכן, יוצרים את השיטה _pullStatus
באופן הבא:
lib/app_store_purchase_handler.dart
/// Request the App Store for the latest subscription status.
/// Updates all App Store subscriptions in the database.
/// NOTE: This code only handles when a subscription expires as example.
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where(
(element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore,
);
for (final purchase in appStoreSubscriptions) {
final status = await appStoreServerAPI.getAllSubscriptionStatuses(
purchase.orderId,
);
// Obtain all subscriptions for the order ID.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0,
);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(
SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate,
),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
),
);
}
}
}
}
השיטה הזו פועלת כך:
- מקבלים את רשימת המינויים הפעילים מ-Firestore באמצעות IapRepository.
- לכל הזמנה, המערכת שולחת בקשה לסטטוס המינוי אל App Store Server API.
- מקבל את העסקה האחרונה עבור רכישת המינוי.
- בודק את תאריך התפוגה.
- הסטטוס של המינוי מתעדכן ב-Firestore, ואם תוקף המינוי פג הוא יסומן ככזה.
לבסוף, מוסיפים את כל הקוד הנדרש כדי להגדיר את הגישה ל-API של App Store Server:
bin/server.dart
import 'package:app_store_server_sdk/app_store_server_sdk.dart'; // Add this import
import 'package:firebase_backend_dart/constants.dart'; // And this one.
// add from here
final subscriptionKeyAppStore = File(
'assets/SubscriptionKey.p8',
).readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // Add this argument
),
};
הגדרה של App Store
לאחר מכן מגדירים את App Store:
- מתחברים ל-App Store Connect ובוחרים באפשרות Users and Access (משתמשים וגישה).
- עוברים אל Integrations > Keys > In-App Purchase (שילובים > מפתחות > רכישה מתוך האפליקציה).
- מקישים על סמל הפלוס כדי להוסיף עוד אחד.
- נותנים לו שם, כמו Codelab key.
- מורידים את קובץ ה-p8 שמכיל את המפתח.
- מעתיקים אותו לתיקיית הנכסים, עם השם
SubscriptionKey.p8
. - מעתיקים את מזהה המפתח מהמפתח החדש שנוצר ומגדירים אותו כקבוע
appStoreKeyId
בקובץlib/constants.dart
. - מעתיקים את מזהה הגורם המנפיק שמופיע בראש רשימת המפתחות, ומגדירים אותו כקבוע
appStoreIssuerId
בקובץlib/constants.dart
.
מעקב אחרי רכישות במכשיר
הדרך הכי מאובטחת לעקוב אחרי הרכישות היא בצד השרת, כי קשה לאבטח את הלקוח. עם זאת, צריך למצוא דרך להעביר את המידע בחזרה ללקוח כדי שהאפליקציה תוכל לפעול בהתאם למידע על סטטוס המינוי. אחסון הרכישות ב-Firestore מאפשר לסנכרן את הנתונים עם הלקוח ולעדכן אותם באופן אוטומטי.
כבר הוספתם את IAPRepo לאפליקציה. זהו מאגר Firestore שמכיל את כל נתוני הרכישות של המשתמש ב-List<PastPurchase> purchases
. המאגר מכיל גם את hasActiveSubscription,
, שהוא true אם יש רכישה עם productId storeKeySubscription
עם סטטוס שלא פג. כשהמשתמש לא מחובר לחשבון, הרשימה ריקה.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any(
(element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired,
);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
כל הלוגיקה של הרכישות נמצאת במחלקה DashPurchases
, ושם צריך להוסיף או להסיר מינויים. לכן, מוסיפים את iapRepo
כמאפיין בכיתה ומקצים את iapRepo
בבונה. לאחר מכן, מוסיפים מאזין ישירות בבונה ומסירים את המאזין בשיטה dispose()
. בהתחלה, ה-listener יכול להיות פונקציה ריקה. מכיוון ש-IAPRepo
הוא ChangeNotifier
ואתם קוראים ל-notifyListeners()
בכל פעם שמשתנים הרכישות ב-Firestore, השיטה purchasesUpdate()
נקראת תמיד כשמשתנים המוצרים שנרכשו.
lib/logic/dash_purchases.dart
import '../repo/iap_repo.dart'; // Add this import
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier;
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
IAPRepo iapRepo; // Add this line
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
// Add this.iapRepo as a parameter
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
Future<void> loadPurchases() async {
// Elided.
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate); // Add this line
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
לאחר מכן, מספקים את IAPRepo
ל-constructor ב-main.dart.
אפשר לקבל את המאגר באמצעות context.read
כי הוא כבר נוצר ב-Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(), // Add this line
),
lazy: false,
),
לאחר מכן, כותבים את הקוד של הפונקציה purchaseUpdate()
. בשיטות dash_counter.dart,
ו-applyPaidMultiplier
, המכפיל מוגדר ל-10 או ל-1, בהתאמה, כך שלא צריך לבדוק אם המינוי כבר הוחל.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. הכול מוכן!
חדשות טובות!!! סיימתם את ה-Codelab. אפשר למצוא את הקוד המלא של ה-Codelab הזה בתיקייה complete.
כדי לקבל מידע נוסף, אפשר לנסות את הסדנאות האחרות ללימוד Flutter.