1. מבוא
עדכון אחרון:11 ביולי 2023
כדי להוסיף רכישות מתוך האפליקציה לאפליקציית Flutter, צריך להגדיר בצורה נכונה את חנות האפליקציות ואת חנות Play, לאמת את הרכישה ולהעניק את ההרשאות הדרושות, כמו הטבות למנויים.
ב-Codelab הזה תוסיפו לאפליקציה שלושה סוגים של רכישות מתוך האפליקציה (שסופקו לכם) ומאמתים את הרכישות האלה באמצעות קצה עורפי של Drt ב-Firebase. האפליקציה שסופקה, Dash Clicker, מכילה משחק שבו נעשה שימוש בקמע של ה-Dash בתור מטבע. תוכלו להוסיף את אפשרויות הרכישה הבאות:
- אפשרות רכישה שניתן לחזור עליה ל-2,000 מקפים בו-זמנית.
- רכישה חד-פעמית של שדרוג כדי להפוך את המקף בסגנון הישן למקף בסגנון מודרני.
- מינוי שמכפיל את מספר הקליקים שנוצרו באופן אוטומטי.
אפשרות הרכישה הראשונה מעניקה למשתמש הטבה ישירה של 2000 מקפים. הם זמינים ישירות למשתמש ואפשר לקנות אותם פעמים רבות. המוצר הזה נקרא מוצר מתכלה כי אפשר לצרוך אותו ישירות ואפשר לצרוך אותו כמה פעמים.
האפשרות השנייה משדרגת את המקף למקף יפה יותר. אפשר לרכוש את הפריט רק פעם אחת, והפריט הזה זמין ללא הגבלת זמן. רכישה כזו נקראת 'לא צריכה' כי האפליקציה לא יכולה להשתמש בה אבל היא תקפה לתמיד.
האפשרות השלישית והאחרונה לרכישה היא מינוי. בזמן שהמינוי פעיל, המשתמש יקבל מקפים מהר יותר, אבל כשהוא יפסיק לשלם על המינוי, גם ההטבות ייעלמו.
השירות לקצה העורפי (שזמין גם עבורכם) פועל כאפליקציית Drt, מאמת שהרכישות מתבצעות ומאחסן אותן באמצעות Firestore. אנחנו משתמשים ב-Firestore כדי להקל על התהליך, אבל באפליקציה לסביבת הייצור אפשר להשתמש בכל סוג של שירות לקצה העורפי.
מה תפַתחו
- בחרת להרחיב את האפליקציה כך שתתמוך ברכישות ובמינויים.
- בנוסף, תורחב אפליקציית קצה עורפי של Drt כדי לאמת ולאחסן את הפריטים שנרכשו.
מה תלמדו
- איך להגדיר את App Store ואת חנות Play למוצרים שניתן לרכוש.
- איך ליצור קשר עם החנויות כדי לאמת רכישות ולאחסן אותן ב-Firestore.
- איך לנהל את הרכישות באפליקציה.
למה תזדקק?
- Android Studio 4.1 ואילך
- Xcode 12 ואילך (לפיתוח iOS)
- Flutter SDK
2. הגדרת סביבת הפיתוח
כדי להתחיל את ה-Codelab הזה, צריך להוריד את הקוד ולשנות את מזהה החבילה ל-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
מכיל סדרה של תמונות מצב שבהן עליך להיות בסוף כל שלב בעל שם. הקוד לתחילת הפעולה נמצא בשלב 0, כך שניתן לאתר את הקבצים התואמים בקלות:
cd flutter-codelabs/in_app_purchases/step_00
אם אתם רוצים לדלג קדימה או לראות איך משהו אמור להיראות אחרי שלב כלשהו, כדאי לעיין בספרייה שנקראת אחרי השלב הרצוי. הקוד של השלב האחרון נמצא בתיקייה complete
.
הגדרה של פרויקט התחלתי
יש לפתוח את הפרויקט לתחילת העבודה של step_00
בסביבת הפיתוח המשולבת המועדפת עליך. השתמשנו ב-Android Studio כדי ליצור צילומי מסך, אבל גם Visual Studio Code הוא אפשרות מצוינת. בכל אחד מהעורך, מוודאים שיישומי הפלאגין החדשים ביותר של Drt ו-Flutter מותקנים.
האפליקציות שברצונך ליצור צריכות ליצור קשר עם App Store ועם חנות Play כדי לדעת אילו מוצרים זמינים ובאיזה מחיר. כל אפליקציה מזוהה באמצעות מזהה ייחודי. ב-iOS App Store המזהה הזה נקרא 'מזהה החבילה', ובחנות Play של Android זהו מזהה האפליקציה. המזהים האלה נוצרים בדרך כלל באמצעות סימון שם דומיין הפוך. לדוגמה, כשיוצרים אפליקציה לרכישה מתוך האפליקציה עבור Flutter.dev, נשתמש ב-dev.flutter.inapppurchase
. חושבים על מזהה של האפליקציה. עכשיו מגדירים אותו בהגדרות הפרויקט.
קודם כול צריך להגדיר את מזהה החבילה ל-iOS.
כשהפרויקט פתוח ב-Android Studio, לוחצים לחיצה ימנית על תיקיית iOS, לוחצים על Flutter ופותחים את המודול באפליקציית Xcode.
במבנה התיקיות של Xcode, פרויקט Runner נמצא בחלק העליון, והיעדים Flutter, Runner ו-Products נמצאים מתחת לפרויקט Runner. לוחצים לחיצה כפולה על Runner כדי לערוך את הגדרות הפרויקט ואז לוחצים על Signing & יכולות. מזינים את מזהה החבילה שבחרתם עכשיו בשדה צוות כדי להגדיר את הצוות שלכם.
עכשיו אפשר לסגור את Xcode ולחזור אל Android Studio כדי לסיים את ההגדרה ל-Android. כדי לעשות זאת, פותחים את הקובץ build.gradle
שבקטע android/app,
ומשנים את applicationId
(בשורה 37 בצילום המסך שלמטה) למזהה האפליקציה, שזהה למזהה החבילה של iOS. שימו לב: המזהים בחנויות iOS ו-Android לא חייבים להיות זהים, אבל שמירה על אותם מזהים גורמת פחות לשגיאות ולכן ב-Codelab הזה נשתמש גם במזהים זהים.
3. התקנת הפלאגין
בחלק הזה של ה-Codelab, מתקינים את הפלאגין in_app_purchase.
הוספת תלות ב-pubspec
מוסיפים את in_app_purchase
למפרט של המפרסם על ידי הוספת in_app_purchase
ליחסי התלות במפרט של pubspec:
$ cd app $ flutter pub add in_app_purchase
pubspec.yaml
dependencies:
..
cloud_firestore: ^4.0.3
firebase_auth: ^4.2.2
firebase_core: ^2.5.0
google_sign_in: ^6.0.1
http: ^0.13.4
in_app_purchase: ^3.0.1
intl: ^0.18.0
provider: ^6.0.2
..
לוחצים על pub get כדי להוריד את החבילה, או מריצים את flutter pub get
בשורת הפקודה.
4. הגדרת App Store
כדי להגדיר רכישות מתוך האפליקציה ולבדוק אותן ב-iOS, עליך ליצור אפליקציה חדשה ב-App Store וליצור בה מוצרים שניתן לרכוש. אין צורך לפרסם שום דבר או לשלוח את האפליקציה ל-Apple לבדיקה. יש צורך בחשבון פיתוח כדי לעשות זאת. אם אין לכם חשבון, נרשמים לתוכנית המפתחים של Apple.
הסכמים לגבי אפליקציות בתשלום
כדי להשתמש ברכישות מתוך האפליקציה, צריך גם להיות לכם הסכם פעיל לגבי אפליקציות בתשלום ב-App Store Connect. עוברים אל https://appstoreconnect.apple.com/ ולוחצים על הסכמים, מס ובנקאות.
כאן יופיעו הסכמים לגבי אפליקציות בחינם ובתשלום. הסטטוס של אפליקציות חינמיות צריך להיות פעיל, והסטטוס של האפליקציות בתשלום הוא חדש. חשוב לקרוא את התנאים, לאשר אותם ולהזין את כל המידע הנדרש.
אם הכול מוגדר בצורה נכונה, הסטטוס של אפליקציות בתשלום יהיה פעיל. הפעולה הזו חשובה מאוד כי לא ניתן יהיה לנסות רכישות מתוך האפליקציה ללא הסכם פעיל.
רישום מזהה האפליקציה
צריך ליצור מזהה חדש בפורטל המפתחים של Apple.
בחירת מזהי אפליקציות
בחירת אפליקציה
מספקים תיאור כלשהו ומגדירים את מזהה החבילה כך שיתאים למזהה החבילה כך שיהיה זהה לערך שהוגדר בעבר ב-XCode.
הנחיות נוספות ליצירת מזהה אפליקציה חדש זמינות במרכז העזרה של חשבון הפיתוח .
יצירת אפליקציה חדשה
אפשר ליצור אפליקציה חדשה ב-App Store Connect באמצעות מזהה החבילה הייחודי שלכם.
לקבלת הנחיות נוספות ליצירת אפליקציה חדשה ולניהול הסכמים, אפשר לעיין במרכז העזרה של App Store Connect.
כדי לבדוק את הרכישות מתוך האפליקציה, נדרש משתמש לבדיקה ב-Sandbox. משתמש הבדיקה לא יכול להיות מחובר ל-iTunes – הוא משמש רק לבדיקת רכישות מתוך האפליקציה. אי אפשר להשתמש בכתובת אימייל שכבר משמשת לחשבון Apple. בקטע משתמשים וגישה, עוברים אל בודקים בקטע Sandbox כדי ליצור חשבון Sandbox חדש או לנהל את מזהי Apple הקיימים ב-Sandbox.
עכשיו ניתן להגדיר משתמש Sandbox ב-iPhone על ידי מעבר אל הגדרות > App Store > חשבון Sandbox.
הגדרת הרכישות מתוך האפליקציות
עכשיו מגדירים את שלושת הפריטים שניתן לרכוש:
dash_consumable_2k
: רכישה של פריט שניתן לרכוש שוב ושוב, שמקצה למשתמש 2,000 מקפים (המטבע בתוך האפליקציה) לכל רכישה.dash_upgrade_3d
: 'שדרוג' שלא ניתן להמשיך רכישה שניתן לרכוש אותה פעם אחת בלבד, ומעניקה למשתמש מקף שונה מבחינה קוסמטית ללחוץ עליה.dash_subscription_doubler
: מינוי שמעניק למשתמש מספר מקפים כפול לקליק למשך תקופת המינוי.
עוברים אל רכישות מתוך האפליקציה > ניהול.
יוצרים את הרכישות מתוך האפליקציה עם המזהים שצוינו:
- צריך להגדיר את
dash_consumable_2k
כתוכן שיכול להיות.
יש להשתמש ב-dash_consumable_2k
בתור מזהה המוצר. שם ההפניה משמש רק ב-App Store Connect. פשוט צריך להגדיר אותו ל-dash consumable 2k
ולהוסיף את ההתאמות המקומיות לרכישה. קוראים לרכישה Spring is in the air
עם התיאור 2000 dashes fly out
.
- אפשר להגדיר את
dash_upgrade_3d
כתוכן לא מתכלה.
יש להשתמש ב-dash_upgrade_3d
בתור מזהה המוצר. מגדירים את שם ההפניה כ-dash upgrade 3d
ומוסיפים את ההתאמות לשוק המקומי עבור הרכישה. קוראים לרכישה 3D Dash
עם התיאור Brings your dash back to the future
.
- אפשר להגדיר את
dash_subscription_doubler
כמינוי שמתחדש אוטומטית.
התהליך לגבי מינויים קצת שונה. קודם צריך להגדיר את שם ההפניה ואת מזהה המוצר:
בשלב הבא, צריך ליצור קבוצת מינויים. כאשר כמה מינויים שייכים לאותה קבוצה, משתמש יכול להירשם רק לאחד מהמינויים האלה בו-זמנית, אבל הוא יכול לשדרג או לשדרג לאחור בקלות בין המינויים האלה. צריך פשוט להתקשר לקבוצה הזו subscriptions
.
לאחר מכן, מזינים את משך המינוי ואת ההתאמות לשוק המקומי. צריך לתת למינוי הזה את השם Jet Engine
בתיאור Doubles your clicks
. לוחצים על Save (שמירה).
אחרי שלוחצים על הלחצן שמירה, מוסיפים מחיר למינוי. בוחרים כל מחיר שרוצים.
עכשיו שלוש הרכישות אמורות להופיע ברשימת הרכישות:
5. הגדרת חנות Play
בדומה ל-App Store, נדרש גם חשבון פיתוח לחנות Play. אם עדיין אין לכם חשבון, רושמים חשבון.
איך יוצרים אפליקציה חדשה
יצירת אפליקציה חדשה ב-Google Play Console:
- פותחים את Play Console.
- בוחרים באפשרות כל האפליקציות > יצירת האפליקציה.
- בוחרים את שפת ברירת המחדל ומוסיפים כותרת לאפליקציה. מקלידים את שם האפליקציה שרוצים שיופיע ב-Google Play. אפשר לשנות את השם בהמשך.
- צריך לציין שהאפליקציה היא משחק. אפשר לשנות את הבחירה בשלב מאוחר יותר.
- מציינים אם האפליקציה היא בחינם או בתשלום.
- צריך להוסיף כתובת אימייל שמשתמשי חנות Play יוכלו ליצור איתך קשר לגבי האפליקציה הזו.
- צריך למלא את הנחיות התוכן ואת ההצהרות על חוקי הייצוא של ארה"ב.
- בוחרים באפשרות יצירת אפליקציה.
לאחר יצירת האפליקציה, נכנסים למרכז הבקרה ומשלימים את כל המשימות בקטע הגדרת האפליקציה. כאן מספקים מידע על האפליקציה, כמו סיווגי תוכן וצילומי מסך.
חתימה על הבקשה
כדי לבדוק רכישות מתוך האפליקציה, צריך להעלות לפחות גרסת build אחת ל-Google Play.
לשם כך, צריך שה-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; אל תבדקו את המידע הזה בבקרת מקור ציבורי!
הפניה למאגר המפתחות מהאפליקציה
יוצרים קובץ בשם <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.
העלאת ה-build הראשון
אחרי שהאפליקציה תוגדר לחתימה, תוכלו ליצור את האפליקציה על ידי הרצה של:
flutter build appbundle
הפקודה הזו יוצרת build של גרסה כברירת מחדל. אפשר למצוא את הפלט בכתובת <your app dir>/build/app/outputs/bundle/release/
במרכז הבקרה ב-Google Play Console, עוברים אל גרסה > בדיקה > בדיקה בקבוצה מוגדרת ויצירת גרסה חדשה לבדיקה בקבוצה מוגדרת.
כדי להשתתף ב-Codelab הזה, תמשיכו לאשר את החתימה של Google על האפליקציה, אז כדאי ללחוץ על Continue (המשך) בקטע Play App Signing (חתימת אפליקציה ב-Play) כדי להביע הסכמה.
בשלב הבא, מעלים את ה-App Bundle app-release.aab
שנוצר באמצעות פקודת ה-build.
לוחצים על שמירה ואז על בדיקת הגרסה.
לסיום, לוחצים על התחלת ההשקה לבדיקה פנימית כדי להפעיל את גרסת הבדיקה הפנימית.
הגדרה של משתמשים לבדיקה
כדי לבדוק רכישות מתוך האפליקציה, צריך להוסיף חשבונות Google של הבודקים שלך ב-Google Play Console בשני מיקומים:
- למסלול הבדיקה הספציפי (בדיקה פנימית)
- כבודקי רישיון
קודם כול, מוסיפים את הבודק למסלול הבדיקה הפנימית. חוזרים אל גרסה > בדיקה > בדיקה פנימית ולוחצים על הכרטיסייה בודקים.
כדי ליצור רשימת כתובות אימייל חדשה, לוחצים על יצירה של רשימת כתובות אימייל. נותנים שם לרשימה, ומוסיפים את כתובות האימייל של חשבונות Google שנדרשת להם גישה לבדיקת רכישות מתוך האפליקציה.
לאחר מכן, מסמנים את התיבה שמוצגת לצד הרשימה ולוחצים על שמירת שינויים.
לאחר מכן, מוסיפים את בודקי הרישיונות:
- חוזרים לתצוגה כל האפליקציות ב-Google Play Console.
- עוברים אל הגדרות > בדיקת רישיון.
- יש להוסיף את אותן כתובות אימייל של הבודקים שאמורה להיות להם אפשרות לבדוק רכישות מתוך האפליקציה.
- מגדירים את תגובת הרישיון ל-
RESPOND_NORMALLY
. - לוחצים על שמירת השינויים.
הגדרת הרכישות מתוך האפליקציות
עכשיו צריך להגדיר את הפריטים שניתן לרכוש מתוך האפליקציה.
בדיוק כמו ב-App Store, צריך להגדיר שלוש רכישות שונות:
dash_consumable_2k
: רכישה של פריט שניתן לרכוש שוב ושוב, שמקצה למשתמש 2,000 מקפים (המטבע בתוך האפליקציה) לכל רכישה.dash_upgrade_3d
: 'שדרוג' שלא ניתן להמשיך רכישה שניתן לרכוש אותה פעם אחת בלבד, וכך המשתמש יכול ללחוץ על מקף שונה מבחינה קוסמטית.dash_subscription_doubler
: מינוי שמעניק למשתמש מספר מקפים כפול לקליק למשך תקופת המינוי.
קודם כול, מוסיפים את החומרים המתכלים והלא מתכלים.
- עוברים אל Google Play Console ובוחרים את האפליקציה הרצויה.
- עוברים אל ייצור הכנסות > מוצרים > מוצרים מתוך האפליקציה.
- לוחצים על יצירת מוצר
- מזינים את כל המידע הנדרש לגבי המוצר. צריך לוודא שמזהה המוצר תואם בדיוק למזהה שבו מתכוונים להשתמש.
- לוחצים על שמירה.
- לוחצים על הפעלה.
- חזרה על התהליך ל'שדרוג' שלא ניתן לצריכה רכישה.
בשלב הבא, מוסיפים את המינוי:
- עוברים אל Google Play Console ובוחרים את האפליקציה הרצויה.
- עוברים אל ייצור הכנסות > מוצרים > מינויים.
- לוחצים על יצירת מינוי
- מזינים את כל המידע הנדרש לגבי המינוי. צריך לוודא שמזהה המוצר תואם בדיוק למזהה שבו מתכוונים להשתמש.
- לוחצים על שמירה.
עכשיו הרכישות שלכם אמורות להיות מוגדרות ב-Play Console.
6. מגדירים את Firebase
ב-Codelab הזה משתמשים בשירות לקצה העורפי כדי לאמת משתמשים ולעקוב אחריהם רכישות.
לשימוש בשירות לקצה העורפי יש מספר יתרונות:
- אפשר לאמת עסקאות באופן מאובטח.
- אתם יכולים להגיב לאירועי חיוב מחנויות האפליקציות.
- אפשר לעקוב אחרי הרכישות במסד נתונים.
- המשתמשים לא יוכלו להטעות את האפליקציה כדי שיספקו תכונות פרימיום, על ידי הרצה אחורה של שעון המערכת.
יש הרבה דרכים להגדיר שירות לקצה העורפי, אבל אפשר לעשות את זה באמצעות הפונקציות של הענן ו-Firestore, באמצעות Firebase של Google.
כתיבת הקצה העורפי נחשבת כלא רלוונטית ב-Codelab הזה, לכן הקוד לתחילת הפעולה כבר כולל פרויקט Firebase שמטפל ברכישות בסיסיות כדי לעזור לכם להתחיל.
יישומי פלאגין של Firebase כלולים גם באפליקציה לתחילת העבודה.
כל מה שנשאר לכם לעשות הוא ליצור פרויקט Firebase משלכם, להגדיר גם את האפליקציה וגם את הקצה העורפי של Firebase, ולבסוף לפרוס את הקצה העורפי.
יצירת פרויקט Firebase
נכנסים למסוף Firebase ויוצרים פרויקט Firebase חדש. לצורך הדוגמה הזו, קוראים לפרויקט Dash Clicker.
באפליקציה בקצה העורפי, מקשרים את הרכישות למשתמש מסוים, ולכן נדרש אימות. כדי לעשות את זה, אפשר להשתמש במודול האימות של Firebase עם כניסה לחשבון Google.
- במרכז הבקרה של Firebase, עוברים אל Authentication (אימות) ומפעילים אותו לפי הצורך.
- עוברים לכרטיסייה שיטת כניסה ומפעילים את ספק הכניסה של Google.
צריך להפעיל גם את מסד הנתונים Firestore של Firebase, כי אז צריך להפעיל אותו.
מגדירים את הכללים של Cloud Firestore באופן הבא:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
הגדרת Firebase ל-Flutter
הדרך המומלצת להתקנת Firebase באפליקציית Flutter היא להשתמש ב-FlutterFire CLI. פועלים לפי ההוראות שמפורטות בדף ההגדרה.
כשמריצים את ההגדרה של Flutterfire, צריך לבחור את הפרויקט שיצרתם בשלב הקודם.
$ flutterfire configure
i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
other-flutter-codelab-1 (other-flutter-codelab-1)
other-flutter-codelab-2 (other-flutter-codelab-2)
other-flutter-codelab-3 (other-flutter-codelab-3)
other-flutter-codelab-4 (other-flutter-codelab-4)
<create a new project>
לאחר מכן, מפעילים את האפשרות iOS ו-Android על ידי בחירה בשתי הפלטפורמות.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
כשמוצגת בקשה לביטול firebase_options.dart, צריך לבחור באפשרות 'כן'.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
הגדרת Firebase ל-Android: שלבים נוספים
במרכז הבקרה של Firebase, עוברים אל Project Overview (סקירה כללית של הפרויקט), בוחרים באפשרות Settings (הגדרות) ואז בוחרים בכרטיסייה General (כללי).
גוללים למטה אל האפליקציות שלך ובוחרים באפליקציה dashclicker (android).
כדי לאפשר כניסה ל-Google במצב ניפוי באגים, צריך לספק את טביעת האצבע הגיבוב (hash) SHA-1 של האישור על תוצאות ניפוי הבאגים.
קבלת גיבוב (hash) של אישור החתימה על תוצאות ניפוי באגים
ברמה הבסיסית (root) של הפרויקט באפליקציית 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
לקובצי ה-build של plist
. (מידע נוסף זמין במסמכים בנושא חבילת google_sign_in
). במקרה הזה, הקבצים הם ios/Runner/Info-Debug.plist
ו-ios/Runner/Info-Release.plist
.
צמד המפתח/ערך כבר נוסף, אבל צריך להחליף את הערכים שלו:
- מקבלים את הערך של
REVERSED_CLIENT_ID
מהקובץGoogleService-Info.plist
, בלי הרכיב<string>..</string>
שמקיף אותו. - מחליפים את הערך בקובץ
ios/Runner/Info-Debug.plist
ובקובץios/Runner/Info-Release.plist
מתחת למפתחCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
סיימתם את ההגדרה של Firebase.
7. האזנה לעדכונים בנושא רכישות
בחלק הזה של ה-Codelab, תכינו את האפליקציה לרכישת המוצרים. התהליך הזה כולל האזנה לעדכונים ולשגיאות לגבי רכישות לאחר הפעלת האפליקציה.
האזנה לעדכונים בנושא רכישות
ב-main.dart,
, צריך למצוא את הווידג'ט MyHomePage
שיש לו Scaffold
עם BottomNavigationBar
שמכיל שני דפים. הדף הזה גם יוצר שלושה פריטי Provider
עבור DashCounter
, DashUpgrades,
ו-DashPurchases
. DashCounter
עוקב אחר המספר הנוכחי של מקפים ומוסיף אותם באופן אוטומטי. DashUpgrades
מנהל את השדרוגים שאפשר לקנות באמצעות מקפים. ה-Codelab הזה מתמקד ב-DashPurchases
.
כברירת מחדל, האובייקט של הספק מוגדר כשנשלחת הבקשה הראשונה לאובייקט הזה. האובייקט הזה מאזין ישירות לעדכונים על רכישות כשהאפליקציה מופעלת, לכן צריך להשבית טעינה מדורגת על האובייקט הזה באמצעות lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false,
),
צריך גם מופע של InAppPurchaseConnection
. עם זאת, כדי שהאפליקציה תמשיך לבדוק, צריך דרך לדמות את החיבור. כדי לעשות את זה, יוצרים שיטה של מופע שאפשר לשנות בבדיקה ומוסיפים אותה ל-main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
צריך לעדכן מעט את הבדיקה כדי שהבדיקה תמשיך לפעול. אפשר לחפש את הקוד המלא של TestIAPConnection
ב-widget_test.dart ב-GitHub.
test/widget_test.dart
void main() {
testWidgets('App starts', (WidgetTester tester) async {
IAPConnection.instance = TestIAPConnection();
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
ב-lib/logic/dash_purchases.dart
, עוברים לקוד של DashPurchases ChangeNotifier
. בשלב זה, יש רק DashCounter
שניתן להוסיף למקפים שרכשתם.
צריך להוסיף נכס 'מינוי לשידור', _subscription
(מסוג StreamSubscription<List<PurchaseDetails>> _subscription;
), IAPConnection.instance,
וייבוא. הקוד שמתקבל אמור להיראות כך:
lib/logic/dash_purchases.dart
import 'package:in_app_purchase/in_app_purchase.dart';
class DashPurchases extends ChangeNotifier {
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter);
}
מילת המפתח late
מתווספת אל _subscription
מפני שה-_subscription
מאותחל ב-constructor. הפרויקט הזה מוגדר כלא יכול להיות null כברירת מחדל (NNBD). כלומר, מאפיינים שלא מוצהרים בהם כ-null חייבים להיות בעלי ערך שאינו null. תוחם late
מאפשר לעכב את הגדרת הערך.
ב-constructor, מקבלים את purchaseUpdatedStream
ומתחילים להאזין לשידור. בשיטה dispose()
, מבטלים את המינוי לשידור.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
Future<void> buy(PurchasableProduct product) async {
// omitted
}
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
}
}
עכשיו האפליקציה מקבלת את העדכונים לרכישה, כך שבקטע הבא תבצעו רכישה!
לפני שממשיכים, כדאי להריץ את הבדיקות עם 'flutter test"
' כדי לוודא שהכול מוגדר כמו שצריך.
$ flutter test
00:01 +1: All tests passed!
8. לרכוש דברים.
בחלק הזה של ה-Codelab, מחליפים את המוצרים המדומים הקיימים כרגע במוצרים אמיתיים לרכישה. המוצרים האלה נטענים מהחנויות, מוצגים ברשימה ורוכשים אותם בהקשה על המוצר.
התאמה של מוצר שנרכש
ב-PurchasableProduct
מוצג הדמיה של מוצר. כדי להציג תוכן בפועל, צריך להחליף את המחלקה PurchasableProduct
ב-purchasable_product.dart
בקוד הבא:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
בdash_purchases.dart,
מסירים את רכישות הדמה ומחליפים אותן ברשימה ריקה, List<PurchasableProduct> products = [];
טעינת רכישות זמינות
כדי לאפשר למשתמש לבצע רכישה, צריך לטעון את הרכישות מהחנות. קודם כול בודקים אם החנות זמינה. כשהחנות לא זמינה, אם מגדירים את storeState
לערך notAvailable
תוצג הודעת שגיאה למשתמש.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
כשהחנות זמינה, טוענים את הרכישות הזמינות. בהתאם להגדרה הקודמת של Firebase, צפויים להופיע storeKeyConsumable
, storeKeySubscription,
ו-storeKeyUpgrade
. כאשר רכישה צפויה לא זמינה, מדפיסים את המידע הזה במסוף. כדאי לשלוח את המידע הזה גם לשירות לקצה העורפי.
השיטה await iapConnection.queryProductDetails(ids)
מחזירה גם את המזהים שלא נמצאו וגם את המוצרים שניתן לרכוש שנמצאו. כדי לעדכן את ממשק המשתמש, משתמשים ב-productDetails
מהתשובה, ומגדירים את StoreState
לערך available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
for (var element in response.notFoundIDs) {
debugPrint('Purchase $element not found');
}
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
קוראים לפונקציה loadPurchases()
ב-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);
break;
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
לפני שממשיכים, צריך ליצור את המשתנה _beautifiedDashUpgrade
ולעדכן את המרכיב beautifiedDash
כך שיפנה אליו.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
השיטה _onPurchaseUpdate
מקבלת את עדכוני הרכישה, מעדכנת את הסטטוס של המוצר שמוצג בדף הרכישה ומחילה את הרכישה על לוגיקת המונה. חשוב להתקשר אל completePurchase
לאחר הטיפול ברכישה כדי שהחנות תדע שהרכישה מטופלת כראוי.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(2000);
break;
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. הגדרת הקצה העורפי
לפני שממשיכים למעקב אחרי רכישות ואימות שלהן, צריך להגדיר קצה עורפי של Drt כדי לעשות את זה.
בקטע הזה, צריך לעבוד מהתיקייה dart-backend/
בתור הרמה הבסיסית (root).
ודאו שהכלים הבאים מותקנים אצלכם:
- Dart
- CLI של Firebase
סקירה כללית של הפרויקט הבסיסי
מאחר שחלקים מסוימים מהפרויקט לא נכללים ב-Codelab הזה, הם נכללים בקוד לתחילת הפעולה. כדאי לעבור על מה שכבר מופיע בקוד ההתחלה לפני שמתחילים, כדי להבין איך אתם מתכננים לבנות את הדברים.
הקוד בקצה העורפי יכול לרוץ באופן מקומי במחשב, ואין צורך לפרוס אותו כדי להשתמש בו. עם זאת, צריכה להיות לכם אפשרות להתחבר ממכשיר הפיתוח (Android או iPhone) למכשיר שבו השרת יפעל. לשם כך, הן צריכות להיות באותה רשת, ואתם צריכים לדעת את כתובת ה-IP של המחשב.
מנסים להפעיל את השרת באמצעות הפקודה הבאה:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
הקצה העורפי של Drt משתמש ב-shelf
וב-shelf_router
כדי להציג נקודות קצה ל-API. כברירת מחדל, השרת לא מספק מסלולים. בהמשך יוצרים מסלול שיטפל בתהליך אימות הרכישה.
חלק אחד שכבר כלול בקוד של הסימן לתחילת פעולה הוא IapRepository
ב-lib/iap_repository.dart
. הלמידה איך ליצור אינטראקציה עם Firestore או עם מסדי נתונים באופן כללי לא נחשבת לרלוונטית ל-Codelab הזה. לכן, הקוד לתחילת הפעולה מכיל פונקציות שמאפשרות לך ליצור או לעדכן רכישות ב-Firestore, וגם את כל המחלקות לרכישות האלה.
הגדרת הגישה ל-Firebase
כדי לגשת ל-Firebase Firestore, נדרש מפתח גישה לחשבון שירות. יוצרים מפתח כזה כדי לפתוח את הגדרות הפרויקט ב-Firebase, עוברים לקטע Service accounts ובוחרים באפשרות Generate new key (יצירת מפתח פרטי חדש).
מעתיקים את קובץ ה-JSON שהורדתם לתיקייה assets/
, ומשנים את השם שלו ל-service-account-firebase.json
.
הגדרת גישה ל-Google Play
כדי לגשת לחנות Play לצורך אימות רכישות, עליכם ליצור חשבון שירות עם ההרשאות האלה, ולהוריד את פרטי הכניסה ל-JSON.
- עוברים אל Google Play Console ומתחילים מהדף כל האפליקציות.
- עוברים אל הגדרה > גישה ל-API. אם תתקבל מ-Google Play Console בקשה ליצור פרויקט או לקשר לפרויקט קיים, צריך קודם לעשות זאת ואז לחזור לדף הזה.
- מחפשים את הקטע שבו מגדירים חשבונות שירות ולוחצים על Create new service account.
- לוחצים על הקישור ל-Google Cloud Platform בתיבת הדו-שיח שקופצת.
- בוחרים את הפרויקט הרצוי. אם החשבון לא מופיע, צריך לוודא שאתם מחוברים לחשבון Google הנכון באמצעות הרשימה הנפתחת חשבון בפינה השמאלית העליונה.
- אחרי שבוחרים את הפרויקט, לוחצים על + Create Service Account בסרגל התפריטים העליון.
- נותנים שם לחשבון השירות, ואם רוצים, מוסיפים תיאור שתזכור מה מיועד לו, וממשיכים לשלב הבא.
- מקצים לחשבון השירות את התפקיד עריכה.
- מסיימים את האשף, חוזרים לדף API Access במסוף המפתחים ולוחצים על רענון של חשבונות שירות. החשבון החדש שיצרתם אמור להופיע ברשימה.
- לוחצים על הענקת גישה לחשבון השירות החדש.
- גוללים למטה בדף הבא אל הבלוק נתונים פיננסיים. בוחרים את שתי האפשרויות הצגת נתונים פיננסיים, הזמנות ותגובות לסקר ביטול וניהול הזמנות ומינויים.
- לוחצים על שליחת הזמנה למשתמש.
- עכשיו, כשהחשבון מוגדר, צריך רק ליצור כמה פרטי כניסה. במסוף Cloud, מוצאים את חשבון השירות ברשימה של חשבונות השירות, לוחצים על שלוש הנקודות האנכיות ובוחרים באפשרות ניהול מפתחות.
- יוצרים מפתח JSON חדש ומורידים אותו.
- משנים את השם של הקובץ שהורד לשם
service-account-google-play.json,
ומעבירים אותו לספרייהassets/
.
דבר נוסף שאנחנו צריכים לעשות הוא לפתוח את lib/constants.dart,
ולהחליף את הערך של androidPackageId
במזהה החבילה שבחרת לאפליקציה שלך ל-Android.
הגדרת גישה ל-Apple App Store
כדי לגשת ל-App Store לצורך אימות רכישות, עליכם להגדיר סוד משותף:
- פותחים את App Store Connect.
- עוברים אל האפליקציות שלי ובוחרים את האפליקציה הרצויה.
- בסרגל הניווט של הצד, עוברים אל רכישות מתוך האפליקציה > ניהול.
- בפינה השמאלית העליונה של הרשימה, לוחצים על סוד משותף ספציפי לאפליקציה.
- יוצרים סוד חדש ומעתיקים אותו.
- פותחים את
lib/constants.dart,
ומחליפים את הערך שלappStoreSharedSecret
בסוד המשותף שיצרתם.
קובץ תצורת קבועים
לפני שממשיכים, צריך לוודא שהקבועים הבאים מוגדרים בקובץ lib/constants.dart
:
androidPackageId
: מזהה החבילה שנעשה בו שימוש ב-Android. לדוגמהcom.example.dashclicker
appStoreSharedSecret
: סוד משותף לצורך גישה ל-App Store Connect לביצוע אימות רכישה.bundleId
: מזהה החבילה שנעשה בו שימוש ב-iOS. לדוגמהcom.example.dashclicker
בינתיים אפשר להתעלם משאר הקבועים.
10. אימות הרכישות
התהליך הכללי לאימות רכישות דומה ב-iOS וב-Android.
בשתי החנויות, האפליקציה שלכם מקבלת אסימון כשמתבצעת רכישה.
האסימון נשלח על ידי האפליקציה לשירות לקצה העורפי שלכם, ובסוף התהליך מאמת את הרכישה מול שרתי החנות המתאימה באמצעות האסימון שסופק.
לאחר מכן, השירות לקצה העורפי יכול לבחור לאחסן את הרכישה ולהשיב לאפליקציה אם הרכישה תקפה או לא.
על ידי בקשה לשירות הקצה העורפי לבצע את האימות בחנויות במקום באפליקציה שפועלת במכשיר של המשתמש שלכם, תוכלו למנוע מהמשתמש לקבל גישה לתכונות פרימיום בתשלום על ידי, לדוגמה, הרצה אחורה של שעון המערכת.
הגדרת הצד של Flutter
הגדרת אימות
כשרוצים לשלוח את הרכישות לשירות הקצה העורפי, חשוב לוודא שהמשתמש מאומת בזמן ביצוע הרכישה. רוב לוגיקת האימות כבר נוספה עבורך בפרויקט המקורי. רק חשוב לוודא שלחצן ההתחברות מוצג ב-PurchasePage
כשהמשתמש עדיין לא מחובר לחשבון. צריך להוסיף את הקוד הבא בתחילת שיטת ה-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({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
נקודת הקצה לאימות שיחות דרך האפליקציה
באפליקציה, צריך ליצור את הפונקציה _verifyPurchase(PurchaseDetails purchaseDetails)
שמבצעת קריאה לנקודת הקצה (endpoint) /verifypurchase
בקצה העורפי של Dragt באמצעות http לאחר השיחה.
עליך לשלוח את החנות שנבחרה (google_play
לחנות Play או app_store
ל-App Store), את serverVerificationData
ואת productID
. השרת מחזיר קוד סטטוס שמציין אם הרכישה אומתה.
בקבועים של האפליקציות, מגדירים את כתובת ה-IP של השרת לכתובת ה-IP של המכונה המקומית.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
הוספת firebaseNotifier
עם היצירה של DashPurchases
ב-main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
מוסיפים רכיב getter למשתמש ב-FirebaseNotifier, כדי להעביר את מזהה המשתמש לפונקציית אימות הרכישה.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
מוסיפים את הפונקציה _verifyPurchase
למחלקה DashPurchases
. הפונקציה async
מחזירה ערך בוליאני שמציין אם הרכישה אומתה.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
print('Successfully verified purchase');
return true;
} else {
print('failed request: ${response.statusCode} - ${response.body}');
return false;
}
}
צריך להפעיל את הפונקציה _verifyPurchase
ב-_handlePurchase
לפני שמחילים את הרכישה. צריך להחיל את הרכישה רק אחרי שהיא מאומתת. באפליקציית ייצור, ניתן לציין זאת עוד כדי, למשל, להחיל מינוי לתקופת ניסיון כשהחנות לא זמינה באופן זמני. עם זאת, בדוגמה הזו כדאי לשמור על פשטות, ולהחיל את הרכישה רק לאחר שהרכישה תאומת בהצלחה.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
break;
case storeKeyConsumable:
counter.addBoughtDashes(1000);
break;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
באפליקציה הכול מוכן עכשיו לאימות הרכישות.
הגדרת השירות לקצה העורפי
בשלב הבא צריך להגדיר את הפונקציה של Cloud Functions לאימות רכישות בקצה העורפי.
יצירת רכיבי handler של רכישות
תהליך האימות בשתי החנויות דומה לתהליך האימות, לכן צריך להגדיר מחלקה מופשטת של PurchaseHandler
עם הטמעות נפרדות לכל חנות.
כדי להתחיל, צריך להוסיף קובץ purchase_handler.dart
לתיקייה lib/
, שבה מגדירים כיתה מופשטת מסוג PurchaseHandler
עם שתי שיטות מופשטות לאימות שני סוגים של רכישות: מינויים ומינויים שאינם מינויים.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
כפי שאפשר לראות, כל שיטה דורשת שלושה פרמטרים:
userId:
המזהה של המשתמש המחובר, כך שאפשר לקשר רכישות למשתמש.productData:
נתונים לגבי המוצר. עוד רגע נגדיר את זה.token:
האסימון שסופק למשתמש על ידי החנות.
בנוסף, כדי שיהיה קל יותר להשתמש ברכיבי ה-handler של הרכישות, צריך להוסיף שיטת 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
מכיל מידע בסיסי על המוצרים השונים שניתן לרכוש, כולל מזהה המוצר (שמכונה לפעמים גם SKU) ואת 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
עבור ה-methods של ה-handler; תופנה אליהם מאוחר יותר.
כמו שאולי שמתם לב, ה-constructor לוקח מופע של IapRepository
. ה-handler של הרכישה משתמש במופע הזה כדי לאחסן מידע על רכישות ב-Firestore בשלב מאוחר יותר. כדי לתקשר עם Google Play, עליך להשתמש ב-AndroidPublisherApi
שסופק.
לאחר מכן, חוזרים על הפעולה עבור ה-handler של חנות האפליקציות. יוצרים lib/app_store_purchase_handler.dart
, ומוסיפים מחלקה שתרחיב שוב את PurchaseHandler
:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
נהדר! עכשיו יש לך שני רכיבי handler של רכישה. בשלב הבא ניצור את נקודת הקצה של ה-API לאימות רכישה.
שימוש ברכיבי handler של רכישות
פותחים את 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);
}
({
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');
}
}
הקוד שלמעלה מבצע את הפעולות הבאות:
- מגדירים נקודת קצה (endpoint) מסוג POST, שהקריאה שלה תתבצע מהאפליקציה שיצרתם קודם.
- מפענחים את המטען הייעודי (payload) של JSON ומחלצים את המידע הבא:
userId
: מזהה המשתמש שמחובר עכשיוsource
: החנות בשימוש,app_store
אוgoogle_play
.productData
: התקבל מ-productDataMap
שיצרת בעבר.token
: מכילה את נתוני האימות שצריך לשלוח לחנויות.- מפעילים קריאה ל-method
verifyPurchase
, עבורGooglePlayPurchaseHandler
אוAppStorePurchaseHandler
, בהתאם למקור. - אם האימות הושלם בהצלחה, השיטה תחזיר
Response.ok
ללקוח. - אם האימות נכשל, השיטה מחזירה
Response.internalServerError
ללקוח.
אחרי שיוצרים את נקודת הקצה ב-API, צריך להגדיר את שני רכיבי ה-handler של הרכישות. לשם כך, תצטרכו לטעון את המפתחות של חשבונות השירות שקיבלתם בשלב הקודם ולהגדיר את הגישה לשירותים השונים, כולל Android Publisher API ו-Firebase Firestore API. לאחר מכן, יוצרים את שני רכיבי ה-handler של הרכישות עם יחסי התלות השונים:
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: הטמעה של בורר הרכישות
לאחר מכן, ממשיכים בהטמעת ה-handler של רכישות ב-Google Play.
Google כבר מספקת חבילות Drt לאינטראקציה עם ממשקי ה-API שדרושים לך כדי לאמת רכישות. אתחלת אותם בקובץ server.dart
ועכשיו אתה משתמש בהם במחלקה GooglePlayPurchaseHandler
.
הטמעת ה-handler לרכישות שאינן מסוג מינוי:
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;
}
אפשר לעדכן את ה-handler של רכישת מינוי באופן דומה:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
כדאי להוסיף את השיטה הבאה כדי להקל על הניתוח של מזהי הזמנה, וגם שתי שיטות לניתוח סטטוס הרכישה.
lib/google_play_purchase_handler.dart
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
הרכישות שלכם ב-Google Play אמורות להיות מאומתות ומאוחסנות במסד הנתונים.
לאחר מכן, עוברים לרכישות ב-App Store ל-iOS.
אימות רכישות ב-iOS: הטמעה של ה-handler של רכישה
כדי לאמת רכישות באמצעות App Store, קיימת חבילת Drt של צד שלישי בשם 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, גם למינויים וגם למשתמשים שאינם מינויים. כלומר, אפשר להשתמש באותה לוגיקה לשני ה-handlers. ממזגים אותם יחד כדי שיקראו לאותו הטמעה:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
בשלב הזה, מטמיעים את handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
עכשיו הרכישות שלך ב-App Store אמורות להיות מאומתות ומאוחסנות במסד הנתונים.
הפעלת הקצה העורפי
בשלב הזה אפשר להריץ את dart bin/server.dart
כדי להציג את נקודת הקצה /verifypurchase
.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. מעקב אחר רכישות
הדרך המומלצת לעקוב אחרי הביצועים של המשתמשים רכישות הן בשירות לקצה העורפי. הסיבה לכך היא שהקצה העורפי שלך יכול להגיב לאירועים מהחנות, ולכן הוא פחות נחשף למידע מיושן בגלל שמירה במטמון, וגם הוא פחות חשוף לשינויים.
קודם כול, צריך להגדיר את העיבוד של אירועי החנות בקצה העורפי באמצעות הקצה העורפי של Drt שיצרת.
עיבוד אירועי חנות בקצה העורפי
החנויות יכולות להודיע לקצה העורפי שלכם על כל אירוע חיוב שמתרחש, למשל מתי המינויים מתחדשים. אפשר לעבד את האירועים האלה בקצה העורפי כדי שהרכישות במסד הנתונים יהיו עדכניות. בקטע הזה צריך להגדיר גם את חנות 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
מוגדר להפעיל את ה-method _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);
ולסיום, מעבירים אותו ל-constructor של 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 ולוחצים על + Create Topic.
- נותנים לנושא החדש שם זהה לערך שהוגדר עבור
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
בconstants.ts
. במקרה הזה, צריך לתת את השםplay_billing
. אם בחרת במשהו אחר, חשוב לעדכן אתconstants.ts
. יוצרים את הנושא. - ברשימת נושאי ה-Pub/Sub, לוחצים על שלוש הנקודות במאונך של הנושא שיצרתם ואז לוחצים על הצגת הרשאות.
- בסרגל הצד משמאל, בוחרים באפשרות 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 ולקבל את פרטי המינוי באופן ידני.
הסיבה לכך שה-Codelab הזה מתמקד בפתרון השני היא שצריך לחשוף את השרת לאינטרנט כדי להטמיע את התגובה לפעולה מאתר אחר (webhook).
בסביבת ייצור, רצוי להשתמש בשניהם. התגובה לפעולה מאתר אחר (webhook) לקבלת אירועים מ-App Store וה-API של השרת למקרה שפספסת אירוע או שצריך לבדוק שוב את סטטוס המינוי.
בשלב הראשון פותחים את lib/app_store_purchase_handler.dart
ומוסיפים את התלות של AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
משנים את ה-constructor כדי להוסיף טיימר שיקרא ל-method _pullStatus
. הטיימר הזה יקרא לשיטה _pullStatus
כל 10 שניות. אפשר לשנות את משך הזמן של הטיימר לפי הצרכים שלך.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
לאחר מכן, יוצרים את השיטה _pullStatus באופן הבא:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
השיטה הזו פועלת כך:
- מקבל את רשימת המינויים הפעילים מ-Firestore באמצעות ה-IapRepository.
- סטטוס המינוי של כל הזמנה הוא ל-App Store Server API.
- מקבל את העסקה האחרונה של רכישת המינוי הזו.
- בודקת את תאריך התפוגה.
- מעדכן את סטטוס המינוי ב-Firestore, אם הוא כבר לא בתוקף, הוא יסומן ככזה.
לסיום, מוסיפים את כל הקוד הנדרש כדי להגדיר את הגישה ל-API של שרת האפליקציות ב-App Store:
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. לאחר מכן, מוסיפים את ה-listener ישירות ב-constructor ומסירים אותו בשיטה dispose()
. בהתחלה, ה-listener יכול להיות פשוט פונקציה ריקה. מכיוון ש-IAPRepo
הוא ChangeNotifier
, והמשתמש מבצע קריאה אל notifyListeners()
בכל פעם שהרכישות ב-Firestore משתנות, לכן תמיד מתבצעת קריאה לשיטה purchasesUpdate()
כשמוצרים שנרכשו משתנים.
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
iapRepo.removeListener(purchasesUpdate);
_subscription.cancel();
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
בשלב הבא, צריך לספק את IAPRepo
ל-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();
}
}
עכשיו וידאת שסטטוס המינוי והשדרוג הוא תמיד עדכני בשירות לקצה העורפי ומסונכרן עם האפליקציה. האפליקציה פועלת בהתאם ומחילה את תכונות המינוי והשדרוג על משחק הקליקים של Dasher.
12. הכול מוכן!
כל הכבוד!!! סיימתם את Codelab. הקוד שהושלם עבור ה-Codelab הזה נמצא בתיקייה המלאה.
כדי לקבל מידע נוסף, אפשר לנסות את Flutter codelabs האחרות.