מידע על Codelab זה
1. מבוא
כדי להוסיף רכישות מתוך האפליקציה לאפליקציה ב-Flutter, צריך להגדיר כראוי את חנות האפליקציות ואת חנות Play, לאמת את הרכישה ולהעניק את ההרשאות הנדרשות, כמו הטבות למנויים.
בקודלאב הזה תוסיפו לאפליקציה (שסופקה לכם) שלושה סוגים של רכישות מתוך האפליקציה, ותאמתו את הרכישות האלה באמצעות קצה עורפי של Dart עם Firebase. האפליקציה שציינת, Dash Clicker, מכילה משחק שבו הדמות Dash משמשת כמטבע. מוסיפים את אפשרויות הרכישה הבאות:
- אפשרות רכישה שניתן לחזור עליה ל-2,000 נקודות Dash בבת אחת.
- רכישה חד-פעמית של שדרוג כדי להפוך את Dash בסגנון הישן ל-Dash בסגנון מודרני.
- מינוי שמכפיל את מספר הקליקים שנוצרים באופן אוטומטי.
אפשרות הרכישה הראשונה מעניקה למשתמש הטבה ישירה של 2,000 נקודות Dash. הם זמינים ישירות למשתמש וניתן לקנות אותם כמה פעמים. הנכס הזה נקרא 'לשימוש חד-פעמי' כי הוא נצרך ישירות וניתן לצרוך אותו כמה פעמים.
האפשרות השנייה היא שדרוג של Dash ל-Dash יפה יותר. צריך לרכוש את השירות הזה רק פעם אחת, והוא יהיה זמין לתמיד. רכישה כזו נקראת 'לא ניתנת לשימוש' כי האפליקציה לא יכולה להשתמש בה, אבל היא תקפה לנצח.
אפשרות הרכישה השלישית והאחרונה היא מינוי. כשהמינוי פעיל, המשתמש יקבל את הנקודות מהר יותר, אבל כשהוא יפסיק לשלם על המינוי, גם ההטבות יפסיקו לפעול.
שירות הקצה העורפי (שגם הוא מסופק לכם) פועל כאפליקציית Dart, מאמת את הרכישות ושומר אותן באמצעות Firestore. אנחנו משתמשים ב-Firestore כדי להקל על התהליך, אבל באפליקציה בסביבת הייצור אפשר להשתמש בכל סוג של שירות לקצה העורפי.
מה תפַתחו
- תרחיבו אפליקציה כך שתתמוך ברכישות של פריטים לשימוש חד-פעמי ובמינויים.
- בנוסף, תרחיבו אפליקציית קצה עורפי של Dart כדי לאמת ולאחסן את הפריטים שנרכשו.
מה תלמדו
- איך מגדירים את App Store ואת Play Store עם מוצרים שאפשר לרכוש.
- איך מתקשרים עם החנויות כדי לאמת רכישות ולאחסן אותן ב-Firestore.
- איך מנהלים את הרכישות באפליקציה.
מה נדרש
- Android Studio 4.1 ואילך
- Xcode מגרסה 12 ואילך (לפיתוח ל-iOS)
- Flutter SDK
2. הגדרת סביבת הפיתוח
כדי להתחיל את סדנת הקוד הזו, צריך להוריד את הקוד ולשנות את מזהה החבילה ל-iOS ואת שם החבילה ל-Android.
הורדת הקוד
כדי להעתיק את מאגר GitHub משורת הפקודה, משתמשים בפקודה הבאה:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
לחלופין, אם התקנתם את הכלי cli של GitHub, תוכלו להשתמש בפקודה הבאה:
gh repo clone flutter/codelabs flutter-codelabs
הקוד לדוגמה משובט לספרייה flutter-codelabs
שמכילה את הקוד של אוסף של Codelabs. הקוד של Codelab הזה נמצא ב-flutter-codelabs/in_app_purchases
.
מבנה הספרייה ב-flutter-codelabs/in_app_purchases
מכיל סדרה של קובצי snapshot של המצב שבו אתם אמורים להיות בסוף כל שלב בעל שם. קוד ההתחלה נמצא בשלב 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, המזהה הזה נקרא מזהה החבילה, וב-Google Play ל-Android הוא נקרא מזהה האפליקציה. בדרך כלל, מזהי ה-ID האלה נוצרים באמצעות סימון הפוך של שם הדומיין. לדוגמה, כשאתם יוצרים אפליקציה עם רכישות מתוך האפליקציה עבור 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. התקנת הפלאגין
בקטע הזה של סדנת הקוד, תתקינו את הפלאגין in_app_purchase.
הוספת תלות ב-pubspec
מוסיפים את in_app_purchase
ל-pubspec על ידי הוספת in_app_purchase
ליחסי התלות ב-pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
פותחים את pubspec.yaml
ומוודאים ש-in_app_purchase
מופיע כרשומה בקטע dependencies
ו-in_app_purchase_platform_interface
בקטע dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.6.3
cupertino_icons: ^1.0.8
firebase_auth: ^5.4.2
firebase_core: ^3.11.0
google_sign_in: ^6.2.2
http: ^1.3.0
intl: ^0.20.2
provider: ^6.1.2
in_app_purchase: ^3.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
לוחצים על pub get כדי להוריד את החבילה או מריצים את flutter pub get
בשורת הפקודה.
4. הגדרת App Store
כדי להגדיר רכישות מתוך האפליקציה ולבדוק אותן ב-iOS, צריך ליצור אפליקציה חדשה ב-App Store וליצור בה מוצרים שניתן לרכוש. אין צורך לפרסם משהו או לשלוח את האפליקציה ל-Apple לבדיקה. לשם כך צריך חשבון פיתוח. אם אין לכם חשבון כזה, עליכם להירשם לתוכנית המפתחים של Apple.
הסכמים של אפליקציות בתשלום
כדי להשתמש ברכישות מתוך האפליקציה, צריך גם להיות לכם הסכם פעיל לגבי אפליקציות בתשלום ב-App Store Connect. עוברים לכתובת https://appstoreconnect.apple.com/ ולוחצים על הסכמים, מסים ובנקאות.
כאן יופיעו הסכמים לאפליקציות בחינם ובתשלום. הסטטוס של אפליקציות חינמיות צריך להיות 'פעיל', והסטטוס של אפליקציות בתשלום צריך להיות 'חדש'. חשוב לקרוא את התנאים, לאשר אותם ולהזין את כל המידע הנדרש.
כשהכול מוגדר בצורה תקינה, הסטטוס של האפליקציות בתשלום יהיה פעיל. חשוב מאוד לעשות זאת, כי לא תוכלו לנסות רכישות מתוך האפליקציה בלי הסכם פעיל.
רישום מזהה האפליקציה
יוצרים מזהה חדש ב-Apple Developer Portal. עוברים אל https://developer.apple.com/account/resources/identifiers/list ולוחצים על סמל הפלוס לצד הכותרת Identifiers.
בחירת מזהי אפליקציות
בחירת אפליקציה
נותנים תיאור כלשהו ומגדירים את מזהה החבילה כך שיהיה זהה לערך שהוגדר בעבר ב-XCode.
לקבלת הנחיות נוספות ליצירת מזהה אפליקציה חדש, אפשר לעיין בעזרה בנושא חשבון פיתוח .
יצירת אפליקציה חדשה
יוצרים אפליקציה חדשה ב-App Store Connect עם מזהה החבילה הייחודי.
לקבלת הנחיות נוספות על יצירת אפליקציה חדשה וניהול הסכמים, אפשר לעיין במרכז העזרה של App Store Connect.
כדי לבדוק את הרכישות מתוך האפליקציה, צריך משתמש לבדיקה בסביבת חול. המשתמש הבודק הזה לא צריך להיות מחובר ל-iTunes – הוא משמש רק לבדיקה של רכישות מתוך האפליקציה. לא ניתן להשתמש בכתובת אימייל שכבר משויכת לחשבון Apple. בקטע משתמשים וגישה, עוברים אל Sandbox כדי ליצור חשבון חדש בסביבת Sandbox או לנהל את מזהי Apple הקיימים בסביבת ה-Sandbox.
עכשיו אפשר להגדיר את המשתמש ב-sandbox ב-iPhone. לשם כך, עוברים אל הגדרות > פיתוח > חשבון Apple ב-sandbox.
הגדרת רכישות מתוך האפליקציה
עכשיו מגדירים את שלושת הפריטים שניתן לרכוש:
dash_consumable_2k
: רכישה של פריטים לשימוש חד-פעמי שאפשר לרכוש שוב ושוב, ומעניקה למשתמש 2,000 Dashes (המטבע באפליקציה) לכל רכישה.dash_upgrade_3d
: רכישה של 'שדרוג' שאינו מתכלה, שאפשר לרכוש רק פעם אחת. הרכישה מעניקה למשתמש Dash שונה מבחינה קוסמטית.dash_subscription_doubler
: מינוי שמעניק למשתמש פי שניים יותר קווים דקים לכל קליק למשך תקופת המינוי.
עוברים אל רכישות מתוך האפליקציה.
יוצרים את הרכישות מתוך האפליקציה באמצעות המזהים שצוינו:
- מגדירים את
dash_consumable_2k
כפריט לשימוש. משתמשים ב-dash_consumable_2k
בתור מזהה המוצר. שם ההפניה משמש רק ב-App Store Connect. צריך להגדיר אותו כ-dash consumable 2k
.מגדירים את הזמינות. המוצר צריך להיות זמין במדינה של המשתמש ב-Sandbox.
מוסיפים את התמחור ומגדירים את המחיר כ-
$1.99
או כערך שווה ערך במטבע אחר.מוסיפים את הגרסאות המקומיות של הרכישה. קוראים לרכישה
Spring is in the air
עם2000 dashes fly out
כתיאור.הוספת צילום מסך של הביקורת. התוכן לא רלוונטי אלא אם המוצר נשלח לבדיקה, אבל הוא נדרש כדי שהמוצר יהיה בסטטוס 'מוכן לשליחה', שנחוץ כשהאפליקציה מאחזרת מוצרים מ-App Store.
- מגדירים את
dash_upgrade_3d
כלא מתכלה. משתמשים ב-dash_upgrade_3d
בתור מזהה המוצר. מגדירים את שם ההפניה כ-dash upgrade 3d
. קוראים לרכישה3D Dash
עםBrings your dash back to the future
כתיאור. מגדירים את המחיר כ-$0.99
. מגדירים את הזמינות ומעלים את צילום המסך לבדיקה באותו אופן שבו עושים זאת עבור המוצרdash_consumable_2k
. - מגדירים את
dash_subscription_doubler
כמינוי מתחדש אוטומטית. התהליך של המינויים שונה במקצת. קודם כול, צריך ליצור קבוצת מינויים. כשיש כמה מינויים באותה קבוצה, משתמש יכול להירשם רק לאחד מהם בכל פעם, אבל הוא יכול לשדרג או לשדרג לאחור בקלות בין המינויים האלה. פשוט קוראים לקבוצה הזוsubscriptions
.ומוסיפים לוקליזציה לקבוצת המינויים.
בשלב הבא יוצרים את המינוי. מגדירים את השם של ההפניה כ-
dash subscription doubler
ואת מזהה המוצר כ-dash_subscription_doubler
.בשלב הבא, בוחרים את משך המינוי (שבוע אחד) ואת הגרסאות המקומיות. נותנים שם
Jet Engine
למינוי הזה עם התיאורDoubles your clicks
. מגדירים את המחיר כ-$0.49
. מגדירים את הזמינות ומעלים את צילום המסך לבדיקה באותו אופן שבו עושים זאת עבור המוצרdash_consumable_2k
.
עכשיו המוצרים אמורים להופיע ברשימות:
5. הגדרת חנות Play
בדומה ל-App Store, גם בחנות Play נדרש חשבון פיתוח. אם עדיין אין לכם חשבון, עליכם להירשם.
יצירת אפליקציה חדשה
יוצרים אפליקציה חדשה ב-Google Play Console:
- פותחים את Play Console.
- בוחרים באפשרות כל האפליקציות > יצירת אפליקציה.
- בוחרים שפת ברירת מחדל ומוסיפים שם לאפליקציה. מקלידים את שם האפליקציה כפי שרוצים שהוא יופיע ב-Google Play. אפשר לשנות את השם בהמשך.
- מציינים שהאפליקציה היא משחק. אפשר לשנות את הבחירה בשלב מאוחר יותר.
- מציינים אם האפליקציה חינמית או בתשלום.
- יש להשלים את ההצהרות לגבי הנחיות התוכן וחקיקת הייצוא של ארה"ב.
- בוחרים באפשרות יצירת אפליקציה.
אחרי שיוצרים את האפליקציה, עוברים ללוח הבקרה ומבצעים את כל המשימות בקטע הגדרת האפליקציה. כאן עליכם לספק מידע על האפליקציה, כמו צילומי מסך וסיווגי תוכן.
חותמים על הבקשה
כדי לבדוק רכישות מתוך האפליקציה, צריך להעלות ל-Google Play גרסה אחת לפחות של build.
לשם כך, צריך לחתום על גרסה build של המוצר באמצעות משהו אחר מאשר מפתחות ניפוי הבאגים.
יצירת מאגר מפתחות
אם יש לכם מאגר מפתחות קיים, מדלגים לשלב הבא. אם לא, יוצרים אותו על ידי הפעלת הפקודה הבאה בשורת הפקודה.
ב-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")
}
}
מעכשיו, גרסאות build של האפליקציה ייחתמו באופן אוטומטי.
מידע נוסף על חתימה על האפליקציה זמין במאמר חתימה על האפליקציה בכתובת developer.android.com.
העלאת הגרסה הראשונה ל-build
אחרי שהאפליקציה מוגדרת לחתימה, אפשר לבנות אותה על ידי הפעלת הפקודה:
flutter build appbundle
הפקודה הזו יוצרת גרסה זמינה (release) כברירת מחדל, והפלט נמצא ב-<your app dir>/build/app/outputs/bundle/release/
בלוח הבקרה ב-Google Play Console, עוברים אל בדיקה והשקה > בדיקה > בדיקה בקבוצות מוגדרות ויוצרים גרסה חדשה לבדיקה בקבוצות מוגדרות.
בשלב הבא, מעלים את חבילת האפליקציות app-release.aab
שנוצרה על ידי פקודת ה-build.
לוחצים על שמירה ואז על בדיקת הגרסה.
לסיום, לוחצים על Start rollout to Closed testing (התחלת ההשקה לבדיקות בקבוצות מוגדרות) כדי להפעיל את הגרסה לבדיקות בקבוצות מוגדרות.
הגדרת משתמשי בדיקה
כדי לבדוק רכישות מתוך האפליקציה, צריך להוסיף את חשבונות Google של הבודקים במסוף Google Play בשני מיקומים:
- למסלול הספציפי לבדיקה (בדיקה פנימית)
- בתור בודקי רישיונות
קודם כול, מוסיפים את הבוחן למסלול הבדיקה הפנימית. חוזרים אל בדיקה והשקה > בדיקה > בדיקה פנימית ולוחצים על הכרטיסייה בודקים.
לוחצים על יצירת רשימת כתובות אימייל כדי ליצור רשימה חדשה. נותנים לרשימת המשתתפים שם ומוסיפים את כתובות האימייל של חשבונות Google שצריכים גישה לבדיקה של רכישות מתוך האפליקציה.
לאחר מכן, מסמנים את התיבה שלצד הרשימה ולוחצים על שמירת השינויים.
לאחר מכן מוסיפים את בודקי הרישיונות:
- חוזרים לתצוגה כל האפליקציות ב-Google Play Console.
- עוברים אל הגדרות > בדיקת רישיון.
- מוסיפים את אותן כתובות אימייל של הבודקים שצריכים לבדוק את הרכישות מתוך האפליקציה.
- מגדירים את License response (תגובה לרישיון) לערך
RESPOND_NORMALLY
. - לוחצים על שמירת השינויים.
הגדרת רכישות מתוך האפליקציה
עכשיו נגדיר את הפריטים שאפשר לרכוש באפליקציה.
בדיוק כמו ב-App Store, צריך להגדיר שלוש רכישות שונות:
dash_consumable_2k
: רכישה של פריטים לשימוש חד-פעמי שאפשר לרכוש שוב ושוב, ומעניקה למשתמש 2,000 Dashes (המטבע באפליקציה) לכל רכישה.dash_upgrade_3d
: רכישה של 'שדרוג' שאינו מתכלה, שאפשר לרכוש רק פעם אחת. הרכישה הזו מעניקה למשתמש Dash to click שונה מבחינה קוסמטית.dash_subscription_doubler
: מינוי שמעניק למשתמש פי שניים יותר קווים דקים לכל קליק למשך תקופת המינוי.
קודם כול, מוסיפים את הפריטים החד-פעמיים ואת הפריטים הלא חד-פעמיים.
- נכנסים ל-Google Play Console ובוחרים את האפליקציה.
- עוברים אל מונטיזציה > מוצרים > מוצרים מתוך האפליקציה.
- לוחצים על יצירת מוצר
- מזינים את כל המידע הנדרש לגבי המוצר. חשוב לוודא שמזהה המוצר תואם בדיוק למזהה שבו אתם מתכוונים להשתמש.
- לוחצים על שמירה.
- לוחצים על הפעלה.
- חוזרים על התהליך עבור הרכישה של 'שדרוג' שאינו ניתן לשימוש.
בשלב הבא, מוסיפים את המינוי:
- נכנסים ל-Google Play Console ובוחרים את האפליקציה.
- עוברים אל מונטיזציה > מוצרים > מינויים.
- לוחצים על Create subscription (יצירת מינוי)
- מזינים את כל הפרטים הנדרשים במינוי. חשוב לוודא שמזהה המוצר תואם בדיוק למזהה שבו אתם מתכוונים להשתמש.
- לוחצים על שמירה.
עכשיו רכישות ה-IAP אמורות להיות מוגדרות ב-Play Console.
6. הגדרת Firebase
ב-codelab הזה תלמדו איך להשתמש בשירות לקצה העורפי כדי לאמת את הרכישות של המשתמשים ולעקוב אחריהן.
לשימוש בשירות לקצה העורפי יש כמה יתרונות:
- אפשר לאמת עסקאות בצורה מאובטחת.
- אתם יכולים להגיב לאירועי חיוב מחנויות האפליקציות.
- אפשר לעקוב אחרי הרכישות במסד נתונים.
- המשתמשים לא יוכלו להטעות את האפליקציה ולקבל תכונות פרימיום על ידי החזרת שעון המערכת לאחור.
יש הרבה דרכים להגדיר שירות לקצה עורפי, אבל אנחנו נשתמש ב-Cloud Functions וב-Firestore, באמצעות Firebase של Google.
כתיבת הקצה העורפי לא נכללת בהיקף של סדנת הקוד הזו, ולכן קוד ההתחלה כבר כולל פרויקט Firebase שמטפל ברכישות בסיסיות כדי לעזור לכם להתחיל.
גם הפלאגינים של Firebase כלולים באפליקציית ההתחלה.
כל מה שנותר לעשות הוא ליצור פרויקט Firebase משלכם, להגדיר את האפליקציה ואת הקצה העורפי ב-Firebase ולפרוס את הקצה העורפי.
יצירת פרויקט Firebase
נכנסים למסוף Firebase ויוצרים פרויקט Firebase חדש. בדוגמה הזו, נקרא לפרויקט Dash Clicker.
באפליקציה לקצה העורפי, אתם מקשרים רכישות למשתמש ספציפי, ולכן נדרש אימות. לשם כך, משתמשים במודול האימות של Firebase עם כניסה באמצעות חשבון Google.
- בלוח הבקרה של Firebase, עוברים אל אימות ומפעילים אותו, אם צריך.
- עוברים לכרטיסייה Sign-in method ומפעילים את ספק הכניסה 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 היא להשתמש ב-CLI של FlutterFire. פועלים לפי ההוראות שמפורטות בדף ההגדרה.
כשמריצים את flutterfire configure, בוחרים את הפרויקט שיצרתם בשלב הקודם.
$ flutterfire configure
i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
other-flutter-codelab-1 (other-flutter-codelab-1)
other-flutter-codelab-2 (other-flutter-codelab-2)
other-flutter-codelab-3 (other-flutter-codelab-3)
other-flutter-codelab-4 (other-flutter-codelab-4)
<create a new project>
לאחר מכן, מפעילים את iOS ואת Android על ידי בחירה בשתי הפלטפורמות.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
כשתוצג בקשה לשינוי של firebase_options.dart, בוחרים באפשרות 'כן'.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
הגדרת Firebase ל-Android: שלבים נוספים
בלוח הבקרה של Firebase, עוברים אל סקירה כללית של הפרויקט,בוחרים באפשרות הגדרות ואז בכרטיסייה כללי.
גוללים למטה לקטע האפליקציות שלך ובוחרים באפליקציה dashclicker (android).
כדי לאפשר כניסה באמצעות חשבון Google במצב ניפוי באגים, צריך לספק את טביעת האצבע של גיבוב SHA-1 של אישור ניפוי הבאגים.
אחזור הגיבוב של אישור החתימה לצורך ניפוי באגים
בתיקיית השורש של פרויקט אפליקציית Flutter, עוברים לתיקייה android/
ויוצרים דוח חתימה.
cd android ./gradlew :app:signingReport
תוצג לכם רשימה גדולה של מפתחות חתימה. מכיוון שאתם מחפשים את הגיבוב של אישור ניפוי הבאגים, מחפשים את האישור שבו המאפיינים Variant
ו-Config
מוגדרים כ-debug
. סביר להניח שמאגר המפתחות נמצא בתיקיית הבית שלכם, בקטע .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
מעתיקים את גיבוב ה-SHA-1 וממלאים את השדה האחרון בתיבת הדו-שיח המוצגת כשמוסיפים את האפליקציה.
לבסוף, מריצים שוב את הפקודה flutterfire configure
כדי לעדכן את האפליקציה כך שתכלול את הגדרות החתימה.
$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
הגדרת Firebase ל-iOS: שלבים נוספים
פותחים את ios/Runner.xcworkspace
באמצעות Xcode
. או באמצעות סביבת הפיתוח המשולבת (IDE) המועדפת עליכם.
ב-VSCode, לוחצים לחיצה ימנית על התיקייה ios/
ואז על open in xcode
.
ב-Android Studio, לוחצים לחיצה ימנית על התיקייה ios/
ואז על flutter
ואז על האפשרות open iOS module in Xcode
.
כדי לאפשר כניסה באמצעות חשבון Google ב-iOS, מוסיפים את אפשרות התצורה CFBundleURLTypes
לקובצי ה-plist
של ה-build. (מידע נוסף זמין במסמכי העזרה של חבילת google_sign_in
). במקרה הזה, הקבצים הם ios/Runner/Info.plist
ו-ios/Runner/Info.plist
.
צמד המפתח/ערך כבר נוסף, אבל צריך להחליף את הערכים שלו:
- אחזור הערך של
REVERSED_CLIENT_ID
מהקובץGoogleService-Info.plist
, ללא הרכיב<string>..</string>
שמקיף אותו. - מחליפים את הערך בקובץ
ios/Runner/Info.plist
מתחת למפתחCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
סיימתם את ההגדרה של Firebase.
7. האזנה לעדכונים על רכישות
בקטע הזה של הקודלהב, תכשירו את האפליקציה לרכישת המוצרים. התהליך הזה כולל האזנה לעדכונים ולשגיאות לגבי רכישות אחרי שהאפליקציה מופעלת.
האזנה לעדכונים לגבי רכישות
ב-main.dart,
, מחפשים את הווידג'ט MyHomePage
שיש לו Scaffold
עם BottomNavigationBar
שמכיל שני דפים. הדף הזה יוצר גם שלושה Provider
s עבור DashCounter
, DashUpgrades,
ו-DashPurchases
. DashCounter
עוקב אחרי המספר הנוכחי של מקפים ומוסיף אותם באופן אוטומטי. DashUpgrades
מנהל את השדרוגים שאפשר לקנות באמצעות Dashes. סדנת הקוד הזו מתמקדת ב-DashPurchases
.
כברירת מחדל, האובייקט של ספק מוגדר כשמבקשים אותו בפעם הראשונה. האובייקט הזה מקשיב לעדכוני רכישות ישירות כשהאפליקציה מופעלת, לכן משביתים את טעינת הנתונים בזמן האטה באובייקט הזה באמצעות lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
צריך גם מופע של InAppPurchaseConnection
. עם זאת, כדי שעדיין תוכלו לבדוק את האפליקציה, תצטרכו למצוא דרך לדמות את החיבור. כדי לעשות זאת, יוצרים שיטת מופע שאפשר לשנות אותה בבדיקה ומוסיפים אותה ל-main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
כדי שהבדיקה תמשיך לפעול, צריך לעדכן אותה מעט.
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable(
{required PurchaseParam purchaseParam, bool autoConsume = true}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(ProductDetailsResponse(
productDetails: [],
notFoundIDs: [],
));
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
ב-lib/logic/dash_purchases.dart
, עוברים לקוד של DashPurchases ChangeNotifier
. נכון לעכשיו, יש רק DashCounter
שאפשר להוסיף ל-Dashes שנרכשו.
מוסיפים נכס של מינויים לערוצים, _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
מופעל ב-constructor. הפרויקט הזה מוגדר כ-non-nullable כברירת מחדל (NNBD), כלומר למאפיינים שלא הוגדרו כ-nullable חייב להיות ערך שאינו null. המאפיין late
מאפשר לדחות את הגדרת הערך הזה.
ב-constructor, מקבלים את הסטרימינג purchaseUpdated
ומתחילים להאזין לסטרימינג. בשיטה dispose()
, מבטלים את המינוי לשידור.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
עכשיו האפליקציה מקבלת את עדכוני הרכישות, כך שבקטע הבא תבצעו רכישה.
לפני שממשיכים, מריצים את הבדיקות באמצעות flutter test"
כדי לוודא שכל ההגדרות נכונות.
$ flutter test
00:01 +1: All tests passed!
8. לבצע רכישות
בקטע הזה של סדנת הקוד, תחליפו את מוצרי הדמה הקיימים במוצרים אמיתיים שניתן לרכוש. המוצרים האלה נטענים מהחנויות, מוצגים ברשימה וניתן לרכוש אותם בהקשה על המוצר.
התאמה של PurchasableProduct
PurchasableProduct
מציג מוצר מדומה. כדי לעדכן אותו כך שיציג תוכן בפועל, מחליפים את הכיתה PurchasableProduct
ב-purchasable_product.dart
בקוד הבא:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
ב-dash_purchases.dart,
מסירים את הרכישות המדומינות ומחליפים אותן ברשימת רכישות ריקה, List<PurchasableProduct> products = [];
טעינה של רכישות זמינות
כדי לתת למשתמש אפשרות לבצע רכישה, צריך לטעון את הרכישות מהחנות. קודם כול, בודקים אם החנות זמינה. אם החנות לא זמינה, הגדרת storeState
לערך notAvailable
תציג למשתמש הודעת שגיאה.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
כשהחנות זמינה, אפשר לטעון את הרכישות הזמינות. בהתאם להגדרות הקודמות של 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();
}
לבסוף, משנים את הערך של השדה 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
ומעדכנים את פונקציית ה-getter של beautifiedDash
כך שתצביע עליו.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
השיטה _onPurchaseUpdate
מקבלת את עדכוני הרכישה, מעדכנת את הסטטוס של המוצר שמוצג בדף הרכישה ומחילה את הרכישה על הלוגיקה של המונה. חשוב להתקשר למספר completePurchase
אחרי טיפול ברכישה כדי שהחנות תדע שהטיפול ברכישה בוצע כראוי.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. הגדרת הקצה העורפי
לפני שממשיכים למעקב אחרי רכישות ולאימות שלהן, צריך להגדיר קצה עורפי של Dart שיתמוך בכך.
בקטע הזה, עובדים מהתיקייה dart-backend/
כשורש.
ודאו שהכלים הבאים מותקנים:
- Dart
- Firebase CLI
סקירה כללית של פרויקט הבסיס
חלק מהחלקים של הפרויקט הזה לא נכללים בקוד ההתחלה, כי הם לא רלוונטיים לשיעור ה-Codelab הזה. לפני שמתחילים, כדאי לעבור על מה שכבר נמצא בקוד ההתחלה כדי לקבל מושג איך תרצו לבנות את הדברים.
קוד הקצה העורפי הזה יכול לפעול באופן מקומי במחשב, ואין צורך לפרוס אותו כדי להשתמש בו. עם זאת, צריך להיות אפשרות להתחבר ממכשיר הפיתוח (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 או עם מסדי נתונים באופן כללי לא רלוונטית לסדנת הקוד הזו, קוד ההתחלה מכיל פונקציות ליצירה או לעדכון של רכישות ב-Firestore, וגם את כל הכיתות של הרכישות האלה.
הגדרת הגישה ל-Firebase
כדי לגשת ל-Firebase Firestore, צריך מפתח גישה לחשבון שירות. כדי ליצור מפתח כזה, פותחים את ההגדרות של פרויקט Firebase ועוברים לקטע Service accounts (חשבונות שירות), ואז בוחרים באפשרות Generate new private key (יצירת מפתח פרטי חדש).
מעתיקים את קובץ ה-JSON שהורדתם לתיקייה assets/
ומשנים את השם שלו ל-service-account-firebase.json
.
הגדרת הגישה ל-Google Play
כדי לגשת לחנות Play לאימות רכישות, צריך ליצור חשבון שירות עם ההרשאות האלה ולהוריד את פרטי הכניסה בפורמט JSON שלו.
- נכנסים לדף Google Play Android Developer API במסוף Google Cloud.
אם ב-Google Play Console תופיע בקשה ליצור פרויקט או לקשר פרויקט קיים, עליכם לבצע את הפעולה הנדרשת ואז לחזור לדף הזה.
- לאחר מכן, עוברים אל הדף Service accounts ולוחצים על + Create service account.
- מזינים את Service account name ולוחצים על Create and continue.
- בוחרים את התפקיד Pub/Sub Subscriber ולוחצים על Done.
- אחרי שיוצרים את החשבון, עוברים אל Manage keys (ניהול מפתחות).
- בוחרים באפשרות 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 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
מציג את כפתור ההתחברות כשהמשתמש עדיין לא מחובר. מוסיפים את הקוד הבא לתחילת שיטת ה-build של PurchasePage
:
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
קריאה לנקודת קצה לאימות מהאפליקציה
באפליקציה, יוצרים את הפונקציה _verifyPurchase(PurchaseDetails purchaseDetails)
שמפעילה את נקודת הקצה /verifypurchase
בקצה העורפי של Dart באמצעות קריאה ל-http post.
שולחים את החנות שנבחרה (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) {
return true;
} else {
return false;
}
}
קוראים לפונקציה _verifyPurchase
ב-_handlePurchase
ממש לפני שמחילים את הרכישה. צריך להחיל את הרכישה רק אחרי שהיא מאומתת. באפליקציה בסביבת הייצור, אפשר לציין את האפשרות הזו באופן ספציפי יותר, למשל כדי להחיל מינוי לתקופת ניסיון כשהחנות לא זמינה באופן זמני. עם זאת, בדוגמה הזו נשתמש בגישה פשוטה, ונחיל את הרכישה רק אחרי שהיא תאומת.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(1000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
עכשיו הכל מוכן באפליקציה לאימות הרכישות.
הגדרת שירות הקצה העורפי
בשלב הבא, מגדירים את הקצה העורפי לאימות רכישות בקצה העורפי.
יצירת פונקציות לטיפול ברכישות
מכיוון שתהליך האימות בשתי החנויות כמעט זהה, צריך להגדיר סוג PurchaseHandler
מופשט עם הטמעות נפרדות לכל חנות.
מתחילים בהוספת קובץ purchase_handler.dart
לתיקייה lib/
, שבו מגדירים את הכיתה המצומצמת PurchaseHandler
עם שתי שיטות מצומצממות לאימות של שני סוגים שונים של רכישות: מינויים ולא מינויים.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
כפי שניתן לראות, לכל שיטה נדרשים שלושה פרמטרים:
userId:
המזהה של המשתמש שמחובר, כדי שתוכלו לשייך רכישות למשתמש.productData:
נתונים על המוצר. נגדיר את זה עוד רגע.token:
האסימון שסופק למשתמש על ידי החנות.
בנוסף, כדי להקל על השימוש בטיפולים האלה ברכישות, מוסיפים את השיטה verifyPurchase()
שאפשר להשתמש בה גם למינויים וגם לרכישות אחרות:
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
עכשיו אפשר פשוט להפעיל את verifyPurchase
בשני המקרים, אבל עדיין להשתמש בהטמעות נפרדות!
הכיתה ProductData
מכילה מידע בסיסי על המוצרים השונים שאפשר לרכוש, כולל מזהה המוצר (שנקרא לפעמים גם מק"ט) ו-ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ה-ProductType
יכול להיות מינוי או לא מינוי.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
לבסוף, רשימת המוצרים מוגדרת כמפה באותו קובץ.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
בשלב הבא, מגדירים כמה הטמעות של 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
לשיטות הטיפול. נגיע אליהן בהמשך.
כפי שראיתם, ה-constructor מקבל מופע של IapRepository
. הטיפול ברכישות משתמש במכונה הזו כדי לאחסן מידע על רכישות ב-Firestore בשלב מאוחר יותר. כדי לתקשר עם Google Play, משתמשים ב-AndroidPublisherApi
שסופק.
לאחר מכן, מבצעים את אותו תהליך לטיפול של חנות האפליקציות. יוצרים את lib/app_store_purchase_handler.dart
ומוסיפים שוב כיתה שמרחיבה את PurchaseHandler
:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
נהדר! עכשיו יש לכם שני מודולים לטיפול ברכישות. בשלב הבא נוצר את נקודת הקצה של ה-API לאימות רכישות.
שימוש בטיפול ברכישות
פותחים את bin/server.dart
ויוצרים נקודת קצה של API באמצעות shelf_route
:
bin/server.dart
import 'dart:convert'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart'; // new
import 'package:shelf/shelf.dart'; // new
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 שתופעל מהאפליקציה שיצרתם קודם.
- מפענחים את המטען הייעודי (payload) של ה-JSON ומחליצים את המידע הבא:
userId
: מזהה המשתמש שמחובר עכשיוsource
: החנות שבה נעשה שימוש,app_store
אוgoogle_play
.productData
: מתקבל מה-productDataMap
שיצרתם קודם.token
: מכיל את נתוני האימות שצריך לשלוח לחנויות.- קריאה ל-method
verifyPurchase
, עבורGooglePlayPurchaseHandler
אוAppStorePurchaseHandler
, בהתאם למקור. - אם האימות בוצע בהצלחה, השיטה מחזירה ללקוח
Response.ok
. - אם האימות נכשל, השיטה מחזירה ללקוח
Response.internalServerError
.
אחרי שיוצרים את נקודת הקצה של ה-API, צריך להגדיר את שני הטיפולים ברכישות. לשם כך, צריך לטעון את המפתחות של חשבון השירות שקיבלתם בשלב הקודם ולהגדיר את הגישה לשירותים השונים, כולל Android Publisher API ו-Firebase Firestore API. לאחר מכן יוצרים את שני הטיפולים ברכישות עם יחסי התלות השונים:
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
אימות רכישות ב-Android: הטמעת הטיפול ברכישות
בשלב הבא, ממשיכים להטמיע את הטיפול ברכישות ב-Google Play.
Google כבר מספקת חבילות Dart ליצירת אינטראקציה עם ממשקי ה-API הנדרשים לאימות רכישות. ביצעתם את האיניציאליזציה שלהם בקובץ server.dart
ועכשיו אתם משתמשים בהם בכיתה GooglePlayPurchaseHandler
.
מטמיעים את הטיפול ברכישות שאינן מינויים:
lib/google_play_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
אפשר לעדכן את הטיפול ברכישת המינוי באופן דומה:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
מוסיפים את השיטה הבאה כדי להקל על ניתוח מזהי ההזמנות, ושתי שיטות לניתוח סטטוס הרכישה.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
עכשיו הרכישות שלכם ב-Google Play אמורות להיות מאומתות ונשמרות במסד הנתונים.
בשלב הבא, עוברים לרכישות מ-App Store ל-iOS.
אימות רכישות ב-iOS: הטמעת הטיפול ברכישות
כדי לאמת רכישות דרך App Store, יש חבילה של Dart של צד שלישי בשם app_store_server_sdk
שמקלה על התהליך.
מתחילים ביצירת המכונה ITunesApi
. שימוש בהגדרות של ארגז החול, והפעלת רישום ביומן כדי להקל על ניפוי באגים של שגיאות.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
בניגוד לממשקי ה-API של Google Play, עכשיו App Store משתמש באותם נקודות קצה ל-API גם עבור מינויים וגם עבור פריטים ללא מינויים. המשמעות היא שאפשר להשתמש באותו לוגיקה בשני הטיפולים. ממזגים אותם כך שיפעילו את אותה הטמעה:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
עכשיו מטמיעים את handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
עכשיו הרכישות שלך מ-App Store אמורות להיות מאומתות ונשמרות במסד הנתונים.
הפעלת הקצה העורפי
בשלב הזה אפשר להריץ את 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 מספק אירועי חיוב באמצעות מה שנקרא נושא cloud pub/sub. אלה למעשה תורים של הודעות שאפשר לפרסם בהם הודעות, וגם לצרוך מהם הודעות.
מכיוון שזו פונקציונליות ספציפית ל-Google Play, צריך לכלול את הפונקציונליות הזו ב-GooglePlayPurchaseHandler
.
פותחים את הקובץ lib/google_play_purchase_handler.dart
ומוסיפים את הייבוא של PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
לאחר מכן מעבירים את PubsubApi
אל GooglePlayPurchaseHandler
, ומשנים את ה-constructor של הכיתה כדי ליצור 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/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
הקוד שהוספתם מתקשר עם נושא Pub/Sub מ-Google Cloud כל עשר שניות ומבקש הודעות חדשות. לאחר מכן, מעבדים כל הודעה בשיטה _processMessage
.
השיטה הזו מפענחת את ההודעות הנכנסות ומקבלת את המידע המעודכן על כל רכישה, גם מינויים וגם רכישות אחרות, ומפעילה את handleSubscription
או handleNonSubscription
הקיימים לפי הצורך.
צריך לאשר כל הודעה באמצעות השיטה _askMessage
.
בשלב הבא מוסיפים את יחסי התלות הנדרשים לקובץ server.dart
. מוסיפים את PubsubApi.cloudPlatformScope לתצורת פרטי הכניסה:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
לאחר מכן יוצרים את המופע של PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
ולבסוף, מעבירים אותו למבנה GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
הגדרת Google Play
כתבתם את הקוד לצריכת אירועי חיוב מנושא ה-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 (הצגת ההרשאות).
- בסרגל הצד שמימין, בוחרים באפשרות Add principal.
- מוסיפים את
google-play-developer-notifications@system.gserviceaccount.com
ומקצים לו את התפקיד פרסום הודעות ב-Pub/Sub. - שומרים את השינויים בהרשאות.
- מעתיקים את שם הנושא של הנושא שיצרתם.
- פותחים שוב את Play Console ובוחרים את האפליקציה מהרשימה כל האפליקציות.
- גוללים למטה אל מונטיזציה > הגדרת מונטיזציה.
- ממלאים את הנושא המלא ושומרים את השינויים.
מעכשיו, כל אירועי החיוב ב-Google Play יפורסמו בנושא הזה.
עיבוד אירועי חיוב ב-App Store
לאחר מכן, מבצעים את אותו תהליך לגבי אירועי החיוב ב-App Store. יש שתי דרכים יעילות להטמיע טיפול בעדכונים ברכישות ב-App Store. אחת מהן היא הטמעת webhook שסיפקתם ל-Apple, והיא משתמשת בו כדי לתקשר עם השרת שלכם. הדרך השנייה, שאותה תלמדו ב-codelab הזה, היא להתחבר ל-App Store Server API ולקבל את פרטי המינוי באופן ידני.
הסיבה לכך שהקודלאב הזה מתמקד בפתרון השני היא שצריך לחשוף את השרת לאינטרנט כדי להטמיע את ה-webhook.
בסביבת ייצור, רצוי להשתמש בשניהם. ה-webhook לאיסוף אירועים מ-App Store, ו-Server API למקרה שתפספסו אירוע או שתצטרכו לבדוק שוב את סטטוס המינוי.
קודם כול פותחים את lib/app_store_purchase_handler.dart
ומוסיפים את התלות ב-AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
משנים את המבנה הגנרטיבי כדי להוסיף טיימר שיפעיל את השיטה _pullStatus
. הטיימר הזה יפעיל את השיטה _pullStatus
כל 10 שניות. אתם יכולים לשנות את משך הזמן של הטיימר בהתאם לצרכים שלכם.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
לאחר מכן, יוצרים את השיטה _pullStatus באופן הבא:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
כך פועלת השיטה הזו:
- הפונקציה מקבלת את רשימת המינויים הפעילים מ-Firestore באמצעות IapRepository.
- לכל הזמנה, המערכת שולחת בקשה לסטטוס המינוי ל-App Store Server API.
- הפונקציה מקבלת את העסקה האחרונה לרכישת המינוי.
- בדיקת תאריך התפוגה.
- עדכון סטטוס המינוי ב-Firestore. אם המינוי פג תוקף, הוא יסומן ככזה.
לבסוף, מוסיפים את כל הקוד הנדרש כדי להגדיר את הגישה ל-App Store Server API:
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
הגדרה של App Store
לאחר מכן מגדירים את App Store:
- מתחברים ל-App Store Connect ובוחרים באפשרות Users and Access (משתמשים וגישה).
- עוברים אל Integrations (שילובים) > Keys (מפתחות) > In-App Purchase (רכישות מתוך האפליקציה).
- מקישים על סמל הפלוס כדי להוסיף חשבון חדש.
- נותנים לו שם, למשל 'מפתח Codelab'.
- מורידים את קובץ ה-P8 שמכיל את המפתח.
- מעתיקים אותו לתיקיית הנכסים עם השם
SubscriptionKey.p8
. - מעתיקים את מזהה המפתח מהמפתח החדש שנוצר ומגדירים אותו כקבוע
appStoreKeyId
בקובץlib/constants.dart
. - מעתיקים את מזהה המנפיק ממש בחלק העליון של רשימת המפתחות, ומגדירים אותו כקבוע
appStoreIssuerId
בקובץlib/constants.dart
.
מעקב אחר רכישות במכשיר
הדרך המאובטחת ביותר לעקוב אחרי הרכישות היא בצד השרת, כי קשה לאבטח את הלקוח. עם זאת, צריכה להיות לכם דרך להעביר את המידע בחזרה ללקוח כדי שהאפליקציה תוכל לפעול לפי נתוני סטטוס המינוי. אחסון הרכישות ב-Firestore מאפשר לסנכרן את הנתונים בקלות עם הלקוח ולעדכן אותם באופן אוטומטי.
כבר הוספתם לאפליקציה את IAPRepo, המאגר ב-Firestore שמכיל את כל נתוני הרכישות של המשתמש ב-List<PastPurchase> purchases
. המאגר מכיל גם את הערך hasActiveSubscription,
, שהוא נכון כשיש רכישה עם productId storeKeySubscription
שהסטטוס שלה לא פג. כשהמשתמש לא מחובר לחשבון, הרשימה ריקה.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
כל הלוגיקה של הרכישות נמצאת בכיתה DashPurchases
, ושם צריך להחיל או להסיר מינויים. לכן, מוסיפים את iapRepo
כמאפיין בכיתה ומקצים את iapRepo
ב-constructor. לאחר מכן, מוסיפים מאזין ישירות ב-constructor ומסירים את המאזין בשיטה dispose()
. בשלב הראשון, המאזין יכול להיות פשוט פונקציה ריקה. מכיוון ש-IAPRepo
הוא ChangeNotifier
, וקוראים ל-notifyListeners()
בכל פעם שהרכישות ב-Firestore משתנות, השיטה purchasesUpdate()
תמיד נקראת כשיש שינוי במוצרים שנרכשו.
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
בשלב הבא, מעבירים את IAPRepo
ל-constructor ב-main.dart.
. אפשר לקבל את המאגר באמצעות context.read
כי הוא כבר נוצר ב-Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
בשלב הבא, כותבים את הקוד של הפונקציה purchaseUpdate()
. ב-dash_counter.dart,
, השיטות applyPaidMultiplier
ו-removePaidMultiplier
מגדירות את המכפיל ל-10 או ל-1, בהתאמה, כך שאין צורך לבדוק אם המינוי כבר הופעל. כשהסטטוס של המינוי משתנה, צריך לעדכן גם את הסטטוס של המוצר שניתן לרכוש, כדי שבדף הרכישה יופיע שהוא כבר פעיל. מגדירים את המאפיין _beautifiedDashUpgrade
על סמך הרכישה של השדרוג.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
עכשיו מוודאים שסטטוס המינוי והשדרוג תמיד עדכני בשירות הקצה העורפי ומסונכרן עם האפליקציה. האפליקציה פועלת בהתאם ומחילה את תכונות המינוי והשדרוג על משחק הקליקרים של Dash.
12. הכול מוכן!
חדשות טובות! סיימתם את הקודלהב. הקוד המלא של סדנת הקוד הזו נמצא בתיקייה complete.
מידע נוסף זמין בcodelabs האחרים של Flutter.