מידע על 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
בסביבת הפיתוח המשולבת (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.
כשהפרויקט פתוח ב-Android Studio, לוחצים לחיצה ימנית על התיקייה iOS, לוחצים על Flutter ופותחים את המודול באפליקציית Xcode.
במבנה התיקיות של Xcode, פרויקט Runner נמצא בחלק העליון, והיעדים Flutter, Runner ו-Products נמצאים מתחת לפרויקט Runner. לוחצים לחיצה כפולה על Runner כדי לערוך את הגדרות הפרויקט, ואז לוחצים על Signing & Capabilities. מזינים את מזהה החבילה שבחרתם בשדה צוות כדי להגדיר את הצוות.
עכשיו אפשר לסגור את Xcode ולחזור ל-Android Studio כדי לסיים את ההגדרה ל-Android. לשם כך, פותחים את הקובץ build.gradle
בקטע android/app,
ומשנים את applicationId
(בשורה 37 בצילום המסך שבהמשך) למזהה האפליקציה, זהה למזהה החבילה ב-iOS. שימו לב: המזהים של חנויות iOS ו-Android לא חייבים להיות זהים, אבל אם הם יהיו זהים, יהיה פחות סיכוי לשגיאות. לכן, ב-codelab הזה נשתמש גם במזהים זהים.
3. התקנת הפלאגין
בקטע הזה של סדנת הקוד, תתקינו את הפלאגין in_app_purchase.
הוספת תלות ב-pubspec
מוסיפים את in_app_purchase
ל-pubspec על ידי הוספת in_app_purchase
ליחסי התלות ב-pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
פותחים את pubspec.yaml
ומוודאים ש-in_app_purchase
מופיע כרשומה בקטע dependencies
ו-in_app_purchase_platform_interface
בקטע dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
לוחצים על pub get כדי להוריד את החבילה או מריצים את flutter pub get
בשורת הפקודה.
4. הגדרת App Store
כדי להגדיר רכישות מתוך האפליקציה ולבדוק אותן ב-iOS, צריך ליצור אפליקציה חדשה ב-App Store וליצור בה מוצרים שניתן לרכוש. אין צורך לפרסם משהו או לשלוח את האפליקציה ל-Apple לבדיקה. לשם כך צריך חשבון פיתוח. אם אין לכם חשבון כזה, עליכם להירשם לתוכנית המפתחים של Apple.
הסכמי אפליקציות בתשלום
כדי להשתמש ברכישות מתוך האפליקציה, צריך גם להיות לכם הסכם פעיל לגבי אפליקציות בתשלום ב-App Store Connect. נכנסים לכתובת https://appstoreconnect.apple.com/ ולוחצים על הסכמים, מסים ובנקאות.
כאן יופיעו הסכמים לגבי אפליקציות בחינם ובתשלום. הסטטוס של אפליקציות חינמיות צריך להיות 'פעיל', והסטטוס של אפליקציות בתשלום צריך להיות 'חדש'. חשוב לקרוא את התנאים, לאשר אותם ולהזין את כל המידע הנדרש.
כשהכול מוגדר בצורה נכונה, הסטטוס של האפליקציות בתשלום יהיה פעיל. חשוב מאוד לעשות זאת, כי לא תוכלו לנסות רכישות מתוך האפליקציה בלי הסכם פעיל.
רישום מזהה האפליקציה
יוצרים מזהה חדש בפורטל למפתחים של Apple.
בחירת מזהי אפליקציות
בחירת אפליקציה
נותנים תיאור כלשהו ומגדירים את מזהה החבילה כך שיהיה זהה לערך שהוגדר בעבר ב-XCode.
לקבלת הנחיות נוספות ליצירת מזהה אפליקציה חדש, אפשר לעיין בעזרה בנושא חשבון פיתוח .
יצירת אפליקציה חדשה
יוצרים אפליקציה חדשה ב-App Store Connect עם מזהה החבילה הייחודי.
לקבלת הנחיות נוספות על יצירת אפליקציה חדשה וניהול הסכמים, אפשר לעיין במרכז העזרה של App Store Connect.
כדי לבדוק את הרכישות מתוך האפליקציה, צריך משתמש לבדיקה בסביבת חול. המשתמש הבודק הזה לא צריך להיות מחובר ל-iTunes – הוא משמש רק לבדיקה של רכישות מתוך האפליקציה. לא ניתן להשתמש בכתובת אימייל שכבר משויכת לחשבון Apple. בקטע משתמשים והרשאות גישה, עוברים אל בודקים בקטע ארגז חול כדי ליצור חשבון ארגז חול חדש או לנהל את מזהי Apple הקיימים בארגז החול.
עכשיו אפשר להגדיר את המשתמש ב-sandbox ב-iPhone. לשם כך, עוברים אל הגדרות > App Store > Sandbox-account.
הגדרת רכישות מתוך האפליקציה
עכשיו מגדירים את שלושת הפריטים שניתן לרכוש:
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
ומוסיפים את הגרסאות המקומיות של הרכישה. קוראים לרכישה Spring is in the air
עם 2000 dashes fly out
כתיאור.
- מגדירים את
dash_upgrade_3d
כלא מתכלה.
משתמשים ב-dash_upgrade_3d
בתור מזהה המוצר. מגדירים את שם העזרה כ-dash upgrade 3d
ומוסיפים את הגרסאות המקומיות של הרכישה. קוראים לרכישה 3D Dash
עם Brings your dash back to the future
כתיאור.
- מגדירים את
dash_subscription_doubler
כמינוי מתחדש אוטומטית.
התהליך של המינויים שונה במקצת. קודם צריך להגדיר את שם ההפניה ואת מזהה המוצר:
בשלב הבא צריך ליצור קבוצת מינויים. כשיש כמה מינויים באותה קבוצה, משתמש יכול להירשם רק לאחד מהם בכל פעם, אבל הוא יכול לשדרג או לשדרג לאחור בקלות בין המינויים האלה. פשוט קוראים לקבוצה הזו subscriptions
.
בשלב הבא, מזינים את משך המינוי ואת הגרסאות המקומיות. נותנים שם Jet Engine
ותיאור Doubles your clicks
למינוי. לוחצים על שמירה.
אחרי שלוחצים על הלחצן Save, מוסיפים מחיר למינויים. בוחרים את המחיר הרצוי.
עכשיו שלוש הרכישות אמורות להופיע ברשימת הרכישות:
5. הגדרת חנות Play
בדומה ל-App Store, גם בחנות Play נדרש חשבון פיתוח. אם עדיין אין לכם חשבון, עליכם להירשם.
יצירת אפליקציה חדשה
יוצרים אפליקציה חדשה ב-Google Play Console:
- פותחים את Play Console.
- בוחרים באפשרות כל האפליקציות > יצירת אפליקציה.
- בוחרים שפת ברירת מחדל ומוסיפים שם לאפליקציה. מקלידים את שם האפליקציה כפי שרוצים שהוא יופיע ב-Google Play. אפשר לשנות את השם בהמשך.
- מציינים שהאפליקציה היא משחק. אפשר לשנות את הבחירה בשלב מאוחר יותר.
- מציינים אם האפליקציה חינמית או בתשלום.
- מוסיפים כתובת אימייל שבה משתמשים מחנות Play יוכלו ליצור איתכם קשר בנוגע לאפליקציה הזו.
- יש להשלים את ההצהרות לגבי הנחיות התוכן וחקיקת הייצוא של ארה"ב.
- בוחרים באפשרות יצירת אפליקציה.
אחרי שיוצרים את האפליקציה, עוברים ללוח הבקרה ומבצעים את כל המשימות בקטע הגדרת האפליקציה. כאן עליכם לספק מידע על האפליקציה, כמו סיווג התוכן ותמונות מסך.
חותמים על הבקשה
כדי לבדוק רכישות מתוך האפליקציה, צריך להעלות ל-Google Play גרסה אחת לפחות של 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
.
מוסיפים את פרטי מאגר המפתחות מקובץ המאפיינים לפני הבלוק android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
טוענים את הקובץ key.properties
לאובייקט keystoreProperties
.
מוסיפים את הקוד הבא לפני הבלוק buildTypes
:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
מגדירים את הבלוק signingConfigs
בקובץ build.gradle
של המודול עם פרטי תצורת החתימה:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
מעכשיו, גרסאות build של האפליקציה ייחתמו באופן אוטומטי.
מידע נוסף על חתימה על האפליקציה זמין במאמר חתימה על האפליקציה בכתובת developer.android.com.
העלאת הגרסה הראשונה
אחרי שהאפליקציה מוגדרת לחתימה, אמורה להיות לכם אפשרות ליצור את האפליקציה על ידי הפעלת הפקודה:
flutter build appbundle
הפקודה הזו יוצרת גרסה זמינה (release) כברירת מחדל, והפלט נמצא ב-<your app dir>/build/app/outputs/bundle/release/
בלוח הבקרה ב-Google Play Console, עוברים אל הפצה > בדיקה > בדיקה בקבוצה מוגדרת ויוצרים גרסה חדשה לבדיקה בקבוצה מוגדרת.
בסדנת הקוד הזו, נשתמש בחתימת Google על האפליקציה, לכן אפשר ללחוץ על המשך בקטע חתימת אפליקציות ב-Play כדי להביע הסכמה.
בשלב הבא, מעלים את חבילת האפליקציות app-release.aab
שנוצרה על ידי פקודת ה-build.
לוחצים על שמירה ואז על בדיקת הגרסה.
לסיום, לוחצים על Start rollout to Internal testing (התחלת ההשקה לבדיקות פנימיות) כדי להפעיל את הגרסה לבדיקות פנימיות.
הגדרת משתמשי בדיקה
כדי לבדוק רכישות מתוך האפליקציה, צריך להוסיף את חשבונות Google של הבודקים במסוף Google Play בשני מיקומים:
- למסלול הספציפי לבדיקה (בדיקה פנימית)
- בתור בודקי רישיונות
קודם כול, מוסיפים את הבוחן למסלול הבדיקה הפנימית. חוזרים אל פרסום > בדיקה > בדיקה פנימית ולוחצים על הכרטיסייה בודקים.
לוחצים על יצירת רשימת כתובות אימייל כדי ליצור רשימה חדשה. נותנים לרשימת המשתתפים שם ומוסיפים את כתובות האימייל של חשבונות Google שצריכים גישה לבדיקה של רכישות מתוך האפליקציה.
לאחר מכן, מסמנים את התיבה שלצד הרשימה ולוחצים על שמירת השינויים.
לאחר מכן מוסיפים את בודקי הרישיונות:
- חוזרים לתצוגה כל האפליקציות ב-Google Play Console.
- עוברים אל הגדרות > בדיקת רישיון.
- מוסיפים את אותן כתובות אימייל של הבודקים שצריכים לבדוק את הרכישות מתוך האפליקציה.
- מגדירים את License response (תגובה לרישיון) לערך
RESPOND_NORMALLY
. - לוחצים על שמירת השינויים.
הגדרת רכישות מתוך האפליקציה
עכשיו נגדיר את הפריטים שאפשר לרכוש באפליקציה.
בדיוק כמו ב-App Store, צריך להגדיר שלוש רכישות שונות:
dash_consumable_2k
: רכישה של פריטים לשימוש חד-פעמי שאפשר לרכוש שוב ושוב, ומעניקה למשתמש 2,000 Dashes (המטבע באפליקציה) לכל רכישה.dash_upgrade_3d
: רכישה של 'שדרוג' שאינו מתכלה, שאפשר לרכוש רק פעם אחת. הרכישה הזו מעניקה למשתמש לחצן Dash שונה מבחינה קוסמטית.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 וממלאים את השדה האחרון בתיבת הדו-שיח המוצגת כשמוסיפים את האפליקציה.
הגדרת Firebase ל-iOS: שלבים נוספים
פותחים את ios/Runnder.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-Debug.plist
ו-ios/Runner/Info-Release.plist
.
צמד המפתח/ערך כבר נוסף, אבל צריך להחליף את הערכים שלו:
- אחזור הערך של
REVERSED_CLIENT_ID
מהקובץGoogleService-Info.plist
, ללא הרכיב<string>..</string>
שמקיף אותו. - מחליפים את הערך בקובץ
ios/Runner/Info-Debug.plist
ובקובץios/Runner/Info-Release.plist
, מתחת למפתחCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
סיימתם את ההגדרה של Firebase.
7. האזנה לעדכונים על רכישות
בקטע הזה של הקודלהב, תתכוננו את האפליקציה לרכישת המוצרים. התהליך הזה כולל האזנה לעדכונים ולשגיאות לגבי רכישות אחרי שהאפליקציה מופעלת.
האזנה לעדכונים לגבי רכישות
ב-main.dart,
, מחפשים את הווידג'ט MyHomePage
שיש לו Scaffold
עם BottomNavigationBar
שמכיל שני דפים. הדף הזה יוצר גם שלושה Provider
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;
}
}
כשהחנות זמינה, אפשר לטעון את הרכישות הזמינות. בהתאם להגדרה הקודמת של Firebase, אמורים להופיע הערכים storeKeyConsumable
, storeKeySubscription,
ו-storeKeyUpgrade
. אם רכישה צפויה לא זמינה, אפשר להדפיס את המידע הזה במסוף. מומלץ גם לשלוח את המידע הזה לשירות הקצה העורפי.
השיטה await iapConnection.queryProductDetails(ids)
מחזירה גם את המזהים שלא נמצאו וגם את המוצרים שניתן לרכוש שנמצאו. משתמשים ב-productDetails
מהתגובה כדי לעדכן את ממשק המשתמש, ומגדירים את StoreState
ל-available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
קוראים לפונקציה loadPurchases()
ב-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 Console ומתחילים מהדף All apps (כל האפליקציות).
- עוברים אל הגדרה > גישה ל-API.
אם ב-Google Play Console תופיע בקשה ליצור פרויקט או לקשר פרויקט קיים, עליכם לבצע את הפעולה הזו קודם ואז לחזור לדף הזה.
- מאתרים את הקטע שבו אפשר להגדיר חשבונות שירות ולוחצים על Create new service account.
- לוחצים על הקישור Google Cloud Platform בתיבת הדו-שיח הקופצת.
- בוחרים את הפרויקט הרצוי. אם היא לא מופיעה, מוודאים שנכנסתם לחשבון Google הנכון ברשימה הנפתחת Account (חשבון) בפינה הימנית העליונה.
- אחרי שבוחרים את הפרויקט, לוחצים על + יצירת חשבון שירות בסרגל התפריטים העליון.
- נותנים שם לחשבון השירות, אפשר גם להוסיף תיאור כדי לזכור למה הוא מיועד, וממשיכים לשלב הבא.
- מקצים לחשבון השירות את התפקיד עריכה.
- מסיימים את האשף, חוזרים לדף API Access במסוף הפיתוח ולוחצים על Refresh service accounts. החשבון החדש שיצרתם אמור להופיע ברשימה.
- לוחצים על Grant access (מתן גישה) לחשבון השירות החדש.
- גוללים למטה בדף הבא, אל הבלוק נתונים פיננסיים. בוחרים גם באפשרות הצגת נתונים פיננסיים, הזמנות ותשובות לסקר הביטול וגם באפשרות ניהול הזמנות ומינויים.
- לוחצים על הזמנת משתמש.
- עכשיו, אחרי שהחשבון מוגדר, צריך ליצור פרטי כניסה. חוזרים למסוף Cloud, מחפשים את חשבון השירות ברשימה של חשבונות השירות, לוחצים על שלוש הנקודות האנכיות ובוחרים באפשרות Manage keys.
- יוצרים מפתח JSON חדש ומורידים אותו.
- משנים את השם של הקובץ שהורדתם ל-
service-account-google-play.json,
ומעבירים אותו לספרייהassets/
.
דבר נוסף שצריך לעשות הוא לפתוח את lib/constants.dart,
ולהחליף את הערך של androidPackageId
במזהה החבילה שבחרתם לאפליקציה ל-Android.
הגדרת גישה ל-Apple App Store
כדי לגשת לחנות האפליקציות לאימות רכישות, צריך להגדיר סוד משותף:
- פותחים את App Store Connect.
- עוברים אל האפליקציות שלי ובוחרים את האפליקציה.
- בתפריט הניווט בסרגל הצד, עוברים אל רכישות מתוך האפליקציות > ניהול.
- בפינה הימנית העליונה של הרשימה, לוחצים על App-Specific Shared Secret (סוד משותף ספציפי לאפליקציה).
- יוצרים סוד חדש ומעתיקים אותו.
- פותחים את הקובץ
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);
}
}
}
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,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
נהדר! עכשיו יש לכם שני מודולים לטיפול ברכישות. בשלב הבא נוצר את נקודת הקצה של ה-API לאימות רכישות.
שימוש בטיפול ברכישות
פותחים את bin/server.dart
ויוצרים נקודת קצה של API באמצעות shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
הקוד שלמעלה מבצע את הפעולות הבאות:
- מגדירים נקודת קצה מסוג POST שתופעל מהאפליקציה שיצרתם קודם.
- מפענחים את המטען הייעודי (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
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
, ומשנים את מגדיר הכיתה כדי ליצור Timer
באופן הבא:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
ה-Timer
מוגדר להפעיל את השיטה _pullMessageFromSubSub
כל עשר שניות. אתם יכולים לשנות את משך הזמן לפי ההעדפה שלכם.
לאחר מכן, יוצרים את _pullMessageFromSubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
הקוד שהוספתם מתקשר עם נושא Pub/Sub מ-Google Cloud כל עשר שניות ומבקש הודעות חדשות. לאחר מכן, מעבדים כל הודעה בשיטה _processMessage
.
השיטה הזו מפענחת את ההודעות הנכנסות ומקבלת את המידע המעודכן על כל רכישה, גם על מינויים וגם על רכישות אחרות, ומפעילה את handleSubscription
או handleNonSubscription
הקיימים לפי הצורך.
צריך לאשר כל הודעה באמצעות השיטה _askMessage
.
בשלב הבא מוסיפים את יחסי התלות הנדרשים לקובץ server.dart
. מוסיפים את PubsubApi.cloudPlatformScope לתצורת פרטי הכניסה:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
לאחר מכן יוצרים את המופע של PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
ולבסוף, מעבירים אותו למבנה GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
הגדרת Google Play
כתבתם את הקוד לצריכת אירועי חיוב מנושא ה-Pub/Sub, אבל לא יצרתם את נושא ה-Pub/Sub ולא מפרסמים אירועי חיוב. הגיע הזמן להגדיר את זה.
קודם כול, יוצרים נושא Pub/Sub:
- נכנסים לדף Cloud Pub/Sub במסוף Google Cloud.
- מוודאים שנמצאים בפרויקט Firebase ולוחצים על + יצירת נושא.
- נותנים שם לנושא החדש, זהה לערך שהוגדר ל-
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
בקובץconstants.ts
. במקרה הזה, נותנים את השםplay_billing
. אם בוחרים משהו אחר, חשוב לעדכן אתconstants.ts
. יוצרים את הנושא. - ברשימת הנושאים ב-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 (משתמשים וגישה).
- עוברים אל סוג מפתח > רכישה מתוך האפליקציה.
- מקישים על סמל הפלוס כדי להוסיף חשבון חדש.
- נותנים לו שם, למשל 'מפתח Codelab'.
- מורידים את קובץ ה-P8 שמכיל את המפתח.
- מעתיקים אותו לתיקיית הנכסים עם השם
SubscriptionKey.p8
. - מעתיקים את מזהה המפתח מהמפתח החדש שנוצר ומגדירים אותו כקבוע
appStoreKeyId
בקובץlib/constants.dart
. - מעתיקים את מזהה המנפיק ממש בחלק העליון של רשימת המפתחות, ומגדירים אותו כקבוע
appStoreIssuerId
בקובץlib/constants.dart
.
מעקב אחר רכישות במכשיר
הדרך המאובטחת ביותר לעקוב אחרי הרכישות היא בצד השרת, כי קשה לאבטח את הלקוח. עם זאת, צריכה להיות לכם דרך להעביר את המידע בחזרה ללקוח כדי שהאפליקציה תוכל לפעול לפי נתוני סטטוס המינוי. אחסון הרכישות ב-Firestore מאפשר לסנכרן את הנתונים בקלות עם הלקוח ולעדכן אותם באופן אוטומטי.
כבר הוספתם את IAPRepo לאפליקציה. זהו המאגר ב-Firestore שמכיל את כל נתוני הרכישות של המשתמש ב-List<PastPurchase> purchases
. המאגר מכיל גם את הערך hasActiveSubscription,
, שהוא נכון כשיש רכישה עם productId storeKeySubscription
שהסטטוס שלה לא פג. כשהמשתמש לא מחובר לחשבון, הרשימה ריקה.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((DocumentSnapshot document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
כל הלוגיקה של הרכישות נמצאת בכיתה DashPurchases
, ושם צריך להחיל או להסיר מינויים. לכן, מוסיפים את iapRepo
כמאפיין בכיתה ומקצים את iapRepo
ב-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.