הוספת רכישות מתוך האפליקציה לאפליקציית Flutter

1. מבוא

עדכון אחרון:11 ביולי 2023

כדי להוסיף רכישות מתוך האפליקציה לאפליקציית Flutter, צריך להגדיר בצורה נכונה את חנות האפליקציות ואת חנות Play, לאמת את הרכישה ולהעניק את ההרשאות הדרושות, כמו הטבות למנויים.

ב-Codelab הזה תוסיפו לאפליקציה שלושה סוגים של רכישות מתוך האפליקציה (שסופקו לכם) ומאמתים את הרכישות האלה באמצעות קצה עורפי של Drt ב-Firebase. האפליקציה שסופקה, Dash Clicker, מכילה משחק שבו נעשה שימוש בקמע של ה-Dash בתור מטבע. תוכלו להוסיף את אפשרויות הרכישה הבאות:

  1. אפשרות רכישה שניתן לחזור עליה ל-2,000 מקפים בו-זמנית.
  2. רכישה חד-פעמית של שדרוג כדי להפוך את המקף בסגנון הישן למקף בסגנון מודרני.
  3. מינוי שמכפיל את מספר הקליקים שנוצרו באופן אוטומטי.

אפשרות הרכישה הראשונה מעניקה למשתמש הטבה ישירה של 2000 מקפים. הם זמינים ישירות למשתמש ואפשר לקנות אותם פעמים רבות. המוצר הזה נקרא מוצר מתכלה כי אפשר לצרוך אותו ישירות ואפשר לצרוך אותו כמה פעמים.

האפשרות השנייה משדרגת את המקף למקף יפה יותר. אפשר לרכוש את הפריט רק פעם אחת, והפריט הזה זמין ללא הגבלת זמן. רכישה כזו נקראת 'לא צריכה' כי האפליקציה לא יכולה להשתמש בה אבל היא תקפה לתמיד.

האפשרות השלישית והאחרונה לרכישה היא מינוי. בזמן שהמינוי פעיל, המשתמש יקבל מקפים מהר יותר, אבל כשהוא יפסיק לשלם על המינוי, גם ההטבות ייעלמו.

השירות לקצה העורפי (שזמין גם עבורכם) פועל כאפליקציית Drt, מאמת שהרכישות מתבצעות ומאחסן אותן באמצעות Firestore. אנחנו משתמשים ב-Firestore כדי להקל על התהליך, אבל באפליקציה לסביבת הייצור אפשר להשתמש בכל סוג של שירות לקצה העורפי.

300123416ebc8dc1.png 7145d0fffe6ea741.png 646317a79be08214.png

מה תפַתחו

  • בחרת להרחיב את האפליקציה כך שתתמוך ברכישות ובמינויים.
  • בנוסף, תורחב אפליקציית קצה עורפי של 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.

942772eb9a73bfaa.png

במבנה התיקיות של Xcode, פרויקט Runner נמצא בחלק העליון, והיעדים Flutter, Runner ו-Products נמצאים מתחת לפרויקט Runner. לוחצים לחיצה כפולה על Runner כדי לערוך את הגדרות הפרויקט ואז לוחצים על Signing & יכולות. מזינים את מזהה החבילה שבחרתם עכשיו בשדה צוות כדי להגדיר את הצוות שלכם.

812f919d965c649a.jpeg

עכשיו אפשר לסגור את Xcode ולחזור אל Android Studio כדי לסיים את ההגדרה ל-Android. כדי לעשות זאת, פותחים את הקובץ build.gradle שבקטע android/app, ומשנים את applicationId (בשורה 37 בצילום המסך שלמטה) למזהה האפליקציה, שזהה למזהה החבילה של iOS. שימו לב: המזהים בחנויות iOS ו-Android לא חייבים להיות זהים, אבל שמירה על אותם מזהים גורמת פחות לשגיאות ולכן ב-Codelab הזה נשתמש גם במזהים זהים.

5c4733ac560ae8c2.png

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/ ולוחצים על הסכמים, מס ובנקאות.

6e373780e5e24a6f.png

כאן יופיעו הסכמים לגבי אפליקציות בחינם ובתשלום. הסטטוס של אפליקציות חינמיות צריך להיות פעיל, והסטטוס של האפליקציות בתשלום הוא חדש. חשוב לקרוא את התנאים, לאשר אותם ולהזין את כל המידע הנדרש.

74c73197472c9aec.png

אם הכול מוגדר בצורה נכונה, הסטטוס של אפליקציות בתשלום יהיה פעיל. הפעולה הזו חשובה מאוד כי לא ניתן יהיה לנסות רכישות מתוך האפליקציה ללא הסכם פעיל.

4a100bbb8cafdbbf.jpeg

רישום מזהה האפליקציה

צריך ליצור מזהה חדש בפורטל המפתחים של Apple.

55d7e592d9a3fc7b.png

בחירת מזהי אפליקציות

13f125598b72ca77.png

בחירת אפליקציה

41ac4c13404e2526.png

מספקים תיאור כלשהו ומגדירים את מזהה החבילה כך שיתאים למזהה החבילה כך שיהיה זהה לערך שהוגדר בעבר ב-XCode.

9d2c940ad80deeef.png

הנחיות נוספות ליצירת מזהה אפליקציה חדש זמינות במרכז העזרה של חשבון הפיתוח .

יצירת אפליקציה חדשה

אפשר ליצור אפליקציה חדשה ב-App Store Connect באמצעות מזהה החבילה הייחודי שלכם.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

לקבלת הנחיות נוספות ליצירת אפליקציה חדשה ולניהול הסכמים, אפשר לעיין במרכז העזרה של App Store Connect.

כדי לבדוק את הרכישות מתוך האפליקציה, נדרש משתמש לבדיקה ב-Sandbox. משתמש הבדיקה לא יכול להיות מחובר ל-iTunes – הוא משמש רק לבדיקת רכישות מתוך האפליקציה. אי אפשר להשתמש בכתובת אימייל שכבר משמשת לחשבון Apple. בקטע משתמשים וגישה, עוברים אל בודקים בקטע Sandbox כדי ליצור חשבון Sandbox חדש או לנהל את מזהי Apple הקיימים ב-Sandbox.

3ca2b26d4e391a4c.jpeg

עכשיו ניתן להגדיר משתמש Sandbox ב-iPhone על ידי מעבר אל הגדרות > App Store > חשבון Sandbox.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

הגדרת הרכישות מתוך האפליקציות

עכשיו מגדירים את שלושת הפריטים שניתן לרכוש:

  • dash_consumable_2k: רכישה של פריט שניתן לרכוש שוב ושוב, שמקצה למשתמש 2,000 מקפים (המטבע בתוך האפליקציה) לכל רכישה.
  • dash_upgrade_3d: 'שדרוג' שלא ניתן להמשיך רכישה שניתן לרכוש אותה פעם אחת בלבד, ומעניקה למשתמש מקף שונה מבחינה קוסמטית ללחוץ עליה.
  • dash_subscription_doubler: מינוי שמעניק למשתמש מספר מקפים כפול לקליק למשך תקופת המינוי.

d156b2f5bac43ca8.png

עוברים אל רכישות מתוך האפליקציה > ניהול.

יוצרים את הרכישות מתוך האפליקציה עם המזהים שצוינו:

  1. צריך להגדיר את dash_consumable_2k כתוכן שיכול להיות.

יש להשתמש ב-dash_consumable_2k בתור מזהה המוצר. שם ההפניה משמש רק ב-App Store Connect. פשוט צריך להגדיר אותו ל-dash consumable 2k ולהוסיף את ההתאמות המקומיות לרכישה. קוראים לרכישה Spring is in the air עם התיאור 2000 dashes fly out.

ec1701834fd8527.png

  1. אפשר להגדיר את dash_upgrade_3d כתוכן לא מתכלה.

יש להשתמש ב-dash_upgrade_3d בתור מזהה המוצר. מגדירים את שם ההפניה כ-dash upgrade 3d ומוסיפים את ההתאמות לשוק המקומי עבור הרכישה. קוראים לרכישה 3D Dash עם התיאור Brings your dash back to the future.

6765d4b711764c30.png

  1. אפשר להגדיר את dash_subscription_doubler כמינוי שמתחדש אוטומטית.

התהליך לגבי מינויים קצת שונה. קודם צריך להגדיר את שם ההפניה ואת מזהה המוצר:

6d29e08dae26a0c4.png

בשלב הבא, צריך ליצור קבוצת מינויים. כאשר כמה מינויים שייכים לאותה קבוצה, משתמש יכול להירשם רק לאחד מהמינויים האלה בו-זמנית, אבל הוא יכול לשדרג או לשדרג לאחור בקלות בין המינויים האלה. צריך פשוט להתקשר לקבוצה הזו subscriptions.

5bd0da17a85ac076.png

לאחר מכן, מזינים את משך המינוי ואת ההתאמות לשוק המקומי. צריך לתת למינוי הזה את השם Jet Engine בתיאור Doubles your clicks. לוחצים על Save (שמירה).

bd1b1d82eeee4cb3.png

אחרי שלוחצים על הלחצן שמירה, מוסיפים מחיר למינוי. בוחרים כל מחיר שרוצים.

d0bf39680ef0aa2e.png

עכשיו שלוש הרכישות אמורות להופיע ברשימת הרכישות:

99d5c4b446e8fecf.png

5. הגדרת חנות Play

בדומה ל-App Store, נדרש גם חשבון פיתוח לחנות Play. אם עדיין אין לכם חשבון, רושמים חשבון.

איך יוצרים אפליקציה חדשה

יצירת אפליקציה חדשה ב-Google Play Console:

  1. פותחים את Play Console.
  2. בוחרים באפשרות כל האפליקציות > יצירת האפליקציה.
  3. בוחרים את שפת ברירת המחדל ומוסיפים כותרת לאפליקציה. מקלידים את שם האפליקציה שרוצים שיופיע ב-Google Play. אפשר לשנות את השם בהמשך.
  4. צריך לציין שהאפליקציה היא משחק. אפשר לשנות את הבחירה בשלב מאוחר יותר.
  5. מציינים אם האפליקציה היא בחינם או בתשלום.
  6. צריך להוסיף כתובת אימייל שמשתמשי חנות Play יוכלו ליצור איתך קשר לגבי האפליקציה הזו.
  7. צריך למלא את הנחיות התוכן ואת ההצהרות על חוקי הייצוא של ארה"ב.
  8. בוחרים באפשרות יצירת אפליקציה.

לאחר יצירת האפליקציה, נכנסים למרכז הבקרה ומשלימים את כל המשימות בקטע הגדרת האפליקציה. כאן מספקים מידע על האפליקציה, כמו סיווגי תוכן וצילומי מסך. 13845badcf9bc1db.png

חתימה על הבקשה

כדי לבדוק רכישות מתוך האפליקציה, צריך להעלות לפחות גרסת 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) כדי להביע הסכמה.

ba98446d9c5c40e0.png

בשלב הבא, מעלים את ה-App Bundle app-release.aab שנוצר באמצעות פקודת ה-build.

לוחצים על שמירה ואז על בדיקת הגרסה.

לסיום, לוחצים על התחלת ההשקה לבדיקה פנימית כדי להפעיל את גרסת הבדיקה הפנימית.

הגדרה של משתמשים לבדיקה

כדי לבדוק רכישות מתוך האפליקציה, צריך להוסיף חשבונות Google של הבודקים שלך ב-Google Play Console בשני מיקומים:

  1. למסלול הבדיקה הספציפי (בדיקה פנימית)
  2. כבודקי רישיון

קודם כול, מוסיפים את הבודק למסלול הבדיקה הפנימית. חוזרים אל גרסה > בדיקה > בדיקה פנימית ולוחצים על הכרטיסייה בודקים.

a0d0394e85128f84.png

כדי ליצור רשימת כתובות אימייל חדשה, לוחצים על יצירה של רשימת כתובות אימייל. נותנים שם לרשימה, ומוסיפים את כתובות האימייל של חשבונות Google שנדרשת להם גישה לבדיקת רכישות מתוך האפליקציה.

לאחר מכן, מסמנים את התיבה שמוצגת לצד הרשימה ולוחצים על שמירת שינויים.

לאחר מכן, מוסיפים את בודקי הרישיונות:

  1. חוזרים לתצוגה כל האפליקציות ב-Google Play Console.
  2. עוברים אל הגדרות > בדיקת רישיון.
  3. יש להוסיף את אותן כתובות אימייל של הבודקים שאמורה להיות להם אפשרות לבדוק רכישות מתוך האפליקציה.
  4. מגדירים את תגובת הרישיון ל-RESPOND_NORMALLY.
  5. לוחצים על שמירת השינויים.

a1a0f9d3e55ea8da.png

הגדרת הרכישות מתוך האפליקציות

עכשיו צריך להגדיר את הפריטים שניתן לרכוש מתוך האפליקציה.

בדיוק כמו ב-App Store, צריך להגדיר שלוש רכישות שונות:

  • dash_consumable_2k: רכישה של פריט שניתן לרכוש שוב ושוב, שמקצה למשתמש 2,000 מקפים (המטבע בתוך האפליקציה) לכל רכישה.
  • dash_upgrade_3d: 'שדרוג' שלא ניתן להמשיך רכישה שניתן לרכוש אותה פעם אחת בלבד, וכך המשתמש יכול ללחוץ על מקף שונה מבחינה קוסמטית.
  • dash_subscription_doubler: מינוי שמעניק למשתמש מספר מקפים כפול לקליק למשך תקופת המינוי.

קודם כול, מוסיפים את החומרים המתכלים והלא מתכלים.

  1. עוברים אל Google Play Console ובוחרים את האפליקציה הרצויה.
  2. עוברים אל ייצור הכנסות > מוצרים > מוצרים מתוך האפליקציה.
  3. לוחצים על יצירת מוצרc8d66e32f57dee21.png
  4. מזינים את כל המידע הנדרש לגבי המוצר. צריך לוודא שמזהה המוצר תואם בדיוק למזהה שבו מתכוונים להשתמש.
  5. לוחצים על שמירה.
  6. לוחצים על הפעלה.
  7. חזרה על התהליך ל'שדרוג' שלא ניתן לצריכה רכישה.

בשלב הבא, מוסיפים את המינוי:

  1. עוברים אל Google Play Console ובוחרים את האפליקציה הרצויה.
  2. עוברים אל ייצור הכנסות > מוצרים > מינויים.
  3. לוחצים על יצירת מינוי32a6a9eefdb71dd0.png
  4. מזינים את כל המידע הנדרש לגבי המינוי. צריך לוודא שמזהה המוצר תואם בדיוק למזהה שבו מתכוונים להשתמש.
  5. לוחצים על שמירה.

עכשיו הרכישות שלכם אמורות להיות מוגדרות ב-Play Console.

6. מגדירים את Firebase

ב-Codelab הזה משתמשים בשירות לקצה העורפי כדי לאמת משתמשים ולעקוב אחריהם רכישות.

לשימוש בשירות לקצה העורפי יש מספר יתרונות:

  • אפשר לאמת עסקאות באופן מאובטח.
  • אתם יכולים להגיב לאירועי חיוב מחנויות האפליקציות.
  • אפשר לעקוב אחרי הרכישות במסד נתונים.
  • המשתמשים לא יוכלו להטעות את האפליקציה כדי שיספקו תכונות פרימיום, על ידי הרצה אחורה של שעון המערכת.

יש הרבה דרכים להגדיר שירות לקצה העורפי, אבל אפשר לעשות את זה באמצעות הפונקציות של הענן ו-Firestore, באמצעות Firebase של Google.

כתיבת הקצה העורפי נחשבת כלא רלוונטית ב-Codelab הזה, לכן הקוד לתחילת הפעולה כבר כולל פרויקט Firebase שמטפל ברכישות בסיסיות כדי לעזור לכם להתחיל.

יישומי פלאגין של Firebase כלולים גם באפליקציה לתחילת העבודה.

כל מה שנשאר לכם לעשות הוא ליצור פרויקט Firebase משלכם, להגדיר גם את האפליקציה וגם את הקצה העורפי של Firebase, ולבסוף לפרוס את הקצה העורפי.

יצירת פרויקט Firebase

נכנסים למסוף Firebase ויוצרים פרויקט Firebase חדש. לצורך הדוגמה הזו, קוראים לפרויקט Dash Clicker.

באפליקציה בקצה העורפי, מקשרים את הרכישות למשתמש מסוים, ולכן נדרש אימות. כדי לעשות את זה, אפשר להשתמש במודול האימות של Firebase עם כניסה לחשבון Google.

  1. במרכז הבקרה של Firebase, עוברים אל Authentication (אימות) ומפעילים אותו לפי הצורך.
  2. עוברים לכרטיסייה שיטת כניסה ומפעילים את ספק הכניסה של Google.

7babb48832fbef29.png

צריך להפעיל גם את מסד הנתונים Firestore של Firebase, כי אז צריך להפעיל אותו.

e20553e0de5ac331.png

מגדירים את הכללים של 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).

b22d46a759c0c834.png

כדי לאפשר כניסה ל-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.

צמד המפתח/ערך כבר נוסף, אבל צריך להחליף את הערכים שלו:

  1. מקבלים את הערך של REVERSED_CLIENT_ID מהקובץ GoogleService-Info.plist, בלי הרכיב <string>..</string> שמקיף אותו.
  2. מחליפים את הערך בקובץ 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 אמורים להופיע אם הם מוגדרים כראוי. לתשומת ליבך: יכול להיות שיעבור זמן מה עד שהרכישות יהיו זמינות כשנכנסים למסופים המתאימים.

ca1a9f97c21e552d.png

צריך לחזור אל 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).

ודאו שהכלים הבאים מותקנים אצלכם:

סקירה כללית של הפרויקט הבסיסי

מאחר שחלקים מסוימים מהפרויקט לא נכללים ב-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 (יצירת מפתח פרטי חדש).

27590fc77ae94ad4.png

מעתיקים את קובץ ה-JSON שהורדתם לתיקייה assets/, ומשנים את השם שלו ל-service-account-firebase.json.

הגדרת גישה ל-Google Play

כדי לגשת לחנות Play לצורך אימות רכישות, עליכם ליצור חשבון שירות עם ההרשאות האלה, ולהוריד את פרטי הכניסה ל-JSON.

  1. עוברים אל Google Play Console ומתחילים מהדף כל האפליקציות.
  2. עוברים אל הגדרה > גישה ל-API. 317fdfb54921f50e.png אם תתקבל מ-Google Play Console בקשה ליצור פרויקט או לקשר לפרויקט קיים, צריך קודם לעשות זאת ואז לחזור לדף הזה.
  3. מחפשים את הקטע שבו מגדירים חשבונות שירות ולוחצים על Create new service account.1e70d3f8d794bebb.png
  4. לוחצים על הקישור ל-Google Cloud Platform בתיבת הדו-שיח שקופצת. 7c9536336dd9e9b4.png
  5. בוחרים את הפרויקט הרצוי. אם החשבון לא מופיע, צריך לוודא שאתם מחוברים לחשבון Google הנכון באמצעות הרשימה הנפתחת חשבון בפינה השמאלית העליונה. 3fb3a25bad803063.png
  6. אחרי שבוחרים את הפרויקט, לוחצים על + Create Service Account בסרגל התפריטים העליון. 62fe4c3f8644acd8.png
  7. נותנים שם לחשבון השירות, ואם רוצים, מוסיפים תיאור שתזכור מה מיועד לו, וממשיכים לשלב הבא. 8a92d5d6a3dff48c.png
  8. מקצים לחשבון השירות את התפקיד עריכה. 6052b7753667ed1a.png
  9. מסיימים את האשף, חוזרים לדף API Access במסוף המפתחים ולוחצים על רענון של חשבונות שירות. החשבון החדש שיצרתם אמור להופיע ברשימה. 5895a7db8b4c7659.png
  10. לוחצים על הענקת גישה לחשבון השירות החדש.
  11. גוללים למטה בדף הבא אל הבלוק נתונים פיננסיים. בוחרים את שתי האפשרויות הצגת נתונים פיננסיים, הזמנות ותגובות לסקר ביטול וניהול הזמנות ומינויים. 75b22d0201cf67e.png
  12. לוחצים על שליחת הזמנה למשתמש. 70ea0b1288c62a59.png
  13. עכשיו, כשהחשבון מוגדר, צריך רק ליצור כמה פרטי כניסה. במסוף Cloud, מוצאים את חשבון השירות ברשימה של חשבונות השירות, לוחצים על שלוש הנקודות האנכיות ובוחרים באפשרות ניהול מפתחות. 853ee186b0e9954e.png
  14. יוצרים מפתח JSON חדש ומורידים אותו. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. משנים את השם של הקובץ שהורד לשם service-account-google-play.json, ומעבירים אותו לספרייה assets/.

דבר נוסף שאנחנו צריכים לעשות הוא לפתוח את lib/constants.dart, ולהחליף את הערך של androidPackageId במזהה החבילה שבחרת לאפליקציה שלך ל-Android.

הגדרת גישה ל-Apple App Store

כדי לגשת ל-App Store לצורך אימות רכישות, עליכם להגדיר סוד משותף:

  1. פותחים את App Store Connect.
  2. עוברים אל האפליקציות שלי ובוחרים את האפליקציה הרצויה.
  3. בסרגל הניווט של הצד, עוברים אל רכישות מתוך האפליקציה > ניהול.
  4. בפינה השמאלית העליונה של הרשימה, לוחצים על סוד משותף ספציפי לאפליקציה.
  5. יוצרים סוד חדש ומעתיקים אותו.
  6. פותחים את lib/constants.dart, ומחליפים את הערך של appStoreSharedSecret בסוד המשותף שיצרתם.

d8b8042470aaeff.png

b72f4565750e2f40.png

קובץ תצורת קבועים

לפני שממשיכים, צריך לוודא שהקבועים הבאים מוגדרים בקובץ 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 עם הטמעות נפרדות לכל חנות.

be50c207c5a2a519.png

כדי להתחיל, צריך להוסיף קובץ 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');
  }
}

הקוד שלמעלה מבצע את הפעולות הבאות:

  1. מגדירים נקודת קצה (endpoint) מסוג POST, שהקריאה שלה תתבצע מהאפליקציה שיצרתם קודם.
  2. מפענחים את המטען הייעודי (payload) של JSON ומחלצים את המידע הבא:
  3. userId: מזהה המשתמש שמחובר עכשיו
  4. source: החנות בשימוש, app_store או google_play.
  5. productData: התקבל מ-productDataMap שיצרת בעבר.
  6. token: מכילה את נתוני האימות שצריך לשלוח לחנויות.
  7. מפעילים קריאה ל-method verifyPurchase, עבור GooglePlayPurchaseHandler או AppStorePurchaseHandler, בהתאם למקור.
  8. אם האימות הושלם בהצלחה, השיטה תחזיר Response.ok ללקוח.
  9. אם האימות נכשל, השיטה מחזירה 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:

  1. נכנסים לדף Cloud Pub/Sub במסוף Google Cloud.
  2. מוודאים שאתם נמצאים בפרויקט Firebase ולוחצים על + Create Topic. d5ebf6897a0a8bf5.png
  3. נותנים לנושא החדש שם זהה לערך שהוגדר עבור GOOGLE_PLAY_PUBSUB_BILLING_TOPIC בconstants.ts. במקרה הזה, צריך לתת את השם play_billing. אם בחרת במשהו אחר, חשוב לעדכן את constants.ts. יוצרים את הנושא. 20d690fc543c4212.png
  4. ברשימת נושאי ה-Pub/Sub, לוחצים על שלוש הנקודות במאונך של הנושא שיצרתם ואז לוחצים על הצגת הרשאות. ea03308190609fb.png
  5. בסרגל הצד משמאל, בוחרים באפשרות Add principal.
  6. כאן, מוסיפים את google-play-developer-notifications@system.gserviceaccount.com ומעניקים לו את התפקיד פרסום הודעות ב-Pub/Sub. 55631ec0549215bc.png
  7. שומרים את השינויים בהרשאות.
  8. מעתיקים את שם הנושא של הנושא שיצרתם.
  9. פותחים שוב את Play Console ובוחרים את האפליקציה שלכם מרשימת כל האפליקציות.
  10. גוללים למטה ועוברים אל ייצור הכנסות > הגדרת מונטיזציה.
  11. ממלאים את הנושא המלא ושומרים את השינויים. 7e5e875dc6ce5d54.png

כל אירועי החיוב ב-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,
          ));
        }
      }
    }
  }

השיטה הזו פועלת כך:

  1. מקבל את רשימת המינויים הפעילים מ-Firestore באמצעות ה-IapRepository.
  2. סטטוס המינוי של כל הזמנה הוא ל-App Store Server API.
  3. מקבל את העסקה האחרונה של רכישת המינוי הזו.
  4. בודקת את תאריך התפוגה.
  5. מעדכן את סטטוס המינוי ב-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:

  1. מתחברים ל-App Store Connect ובוחרים באפשרות Users and Access (משתמשים וגישה).
  2. עוברים אל סוג מפתח > רכישה מתוך האפליקציה.
  3. מקישים על ה'פלוס'. כדי להוסיף סמל חדש.
  4. נותנים למכשיר שם, למשל: 'מפתח Codelab'.
  5. מורידים את קובץ ה-p8 שמכיל את המפתח.
  6. מעתיקים אותו לתיקיית הנכסים בשם SubscriptionKey.p8.
  7. מעתיקים את מזהה המפתח מהמפתח החדש שנוצר ומגדירים אותו באופן קבוע appStoreKeyId בקובץ lib/constants.dart.
  8. מעתיקים את מזהה המנפיק בראש רשימת המפתחות, ומגדירים אותו כקבוע appStoreIssuerId בקובץ lib/constants.dart.

9540ea9ada3da151.png

מעקב אחר רכישות במכשיר

הדרך המאובטחת ביותר לעקוב אחר הרכישות שלכם היא בצד השרת כי הלקוח קשה לאבטח אותו, אבל נדרשת דרך כלשהי להחזיר את המידע ללקוח כדי שהאפליקציה תוכל לפעול על סמך פרטי סטטוס המינוי. אחסון הרכישות ב-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 הזה נמצא android_studio_folder.pngבתיקייה המלאה.

כדי לקבל מידע נוסף, אפשר לנסות את Flutter codelabs האחרות.