1. Wprowadzenie
Dodanie zakupów w aplikacji do aplikacji Flutter wymaga prawidłowego skonfigurowania sklepów z aplikacjami, zweryfikowania zakupu i przyznania niezbędnych uprawnień, takich jak korzyści z subskrypcji.
W tym laboratorium kodowania dodasz do aplikacji (która jest dostępna) 3 rodzaje zakupów w aplikacji i zweryfikujesz je za pomocą backendu Dart z Firebase. Dostarczona aplikacja Dash Clicker zawiera grę, w której maskotka Dash jest używana jako waluta. Dodasz te opcje zakupu:
- Opcja zakupu 2000 Dashes naraz, którą można powtarzać.
- Jednorazowy zakup uaktualnienia, które zmieni stary styl Dash na nowoczesny.
- Subskrypcja, która podwaja automatycznie generowane kliknięcia.
Pierwsza opcja zakupu daje użytkownikowi bezpośrednią korzyść w postaci 2000 Dashes. Są one bezpośrednio dostępne dla użytkownika i można je kupić wiele razy. Jest to produkt konsumpcyjny, ponieważ jest bezpośrednio konsumowany i może być konsumowany wielokrotnie.
Druga opcja to ulepszenie Dash do bardziej atrakcyjnej wersji. Wystarczy kupić go raz, a będzie dostępny na zawsze. Taki zakup nazywa się niekonsumpcyjnym, ponieważ nie może być wykorzystany przez aplikację, ale jest ważny na zawsze.
Trzecią i ostatnią opcją zakupu jest subskrypcja. Gdy subskrypcja jest aktywna, użytkownik szybciej otrzymuje Dash, ale gdy przestanie za nią płacić, korzyści również znikną.
Usługa backendu (również udostępniona) działa jako aplikacja w Dart, weryfikuje zakupy i przechowuje je za pomocą Firestore. Aby ułatwić ten proces, używamy Firestore, ale w aplikacji produkcyjnej możesz użyć dowolnego typu usługi backendu.
Co utworzysz
- Rozszerzysz aplikację, aby obsługiwała zakupy jednorazowe i subskrypcje.
- Rozszerzysz też aplikację backendu Dart, aby weryfikować i przechowywać kupione produkty.
Czego się nauczysz
- Jak skonfigurować App Store i Sklep Play z produktami, które można kupić.
- Jak komunikować się ze sklepami, aby weryfikować zakupy i przechowywać je w Firestore.
- Jak zarządzać zakupami w aplikacji.
Czego potrzebujesz
- Android Studio
- Xcode (do tworzenia aplikacji na iOS)
- Pakiet SDK Flutter
2. Konfigurowanie środowiska programistycznego
Aby rozpocząć ten kurs, pobierz kod i zmień identyfikator pakietu na iOS oraz nazwę pakietu na Androidzie.
Pobieranie kodu
Aby sklonować repozytorium GitHub z wiersza poleceń, użyj tego polecenia:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Jeśli masz zainstalowane narzędzie cli GitHuba, użyj tego polecenia:
gh repo clone flutter/codelabs flutter-codelabs
Przykładowy kod zostanie sklonowany do katalogu flutter-codelabs
, który zawiera kod kolekcji codelabów. Kod do tego ćwiczenia jest w języku flutter-codelabs/in_app_purchases
.
Struktura katalogów w flutter-codelabs/in_app_purchases
zawiera serię zrzutów stanu, w którym powinna znajdować się aplikacja po wykonaniu każdego z kroków. Kod początkowy znajduje się w kroku 0, więc przejdź do niego w ten sposób:
cd flutter-codelabs/in_app_purchases/step_00
Jeśli chcesz przejść do przodu lub zobaczyć, jak coś powinno wyglądać po wykonaniu danego kroku, zajrzyj do katalogu o nazwie odpowiadającej temu krokowi. Kod ostatniego kroku znajduje się w folderze complete
.
Konfigurowanie projektu startowego
Otwórz projekt początkowy z step_00/app
w ulubionym środowisku IDE. Do zrobienia zrzutów ekranu użyliśmy Androida Studio, ale Visual Studio Code to też świetna opcja. W obu edytorach sprawdź, czy są zainstalowane najnowsze wtyczki Dart i Flutter.
Aplikacje, które zamierzasz utworzyć, muszą komunikować się z App Store i Google Play, aby wiedzieć, które produkty są dostępne i w jakiej cenie. Każda aplikacja jest identyfikowana za pomocą unikalnego identyfikatora. W przypadku sklepu App Store na iOS jest to identyfikator pakietu, a w przypadku sklepu Google Play na Androida – identyfikator aplikacji. Te identyfikatory są zwykle tworzone przy użyciu notacji z odwróconą nazwą domeny. Na przykład podczas tworzenia aplikacji do zakupu w aplikacji na flutter.dev użyjesz dev.flutter.inapppurchase
. Wymyśl identyfikator aplikacji, który teraz ustawisz w ustawieniach projektu.
Najpierw skonfiguruj identyfikator pakietu na iOS. Aby to zrobić, otwórz plik Runner.xcworkspace
w aplikacji Xcode.
W strukturze folderów Xcode na górze znajduje się projekt Runner, a pod nim są elementy docelowe Flutter, Runner i Products. Kliknij dwukrotnie Runner, aby edytować ustawienia projektu, a następnie kliknij Signing & Capabilities (Podpisywanie i możliwości). Wpisz wybrany identyfikator pakietu w polu Zespół, aby ustawić zespół.
Możesz teraz zamknąć Xcode i wrócić do Android Studio, aby dokończyć konfigurację na potrzeby Androida. Aby to zrobić, otwórz plik build.gradle.kts
w sekcji android/app,
i zmień applicationId
(w wierszu 24 na zrzucie ekranu poniżej) na identyfikator aplikacji, taki sam jak identyfikator pakietu iOS. Pamiętaj, że identyfikatory sklepów na iOS i Androida nie muszą być identyczne, ale ich identyczność zmniejsza ryzyko błędów, dlatego w tym samouczku będziemy używać identycznych identyfikatorów.
3. Instalowanie wtyczki
W tej części ćwiczenia z programowania zainstalujesz wtyczkę in_app_purchase.
Dodawanie zależności w pliku pubspec
Dodaj in_app_purchase
do pliku pubspec, dodając in_app_purchase
do zależności w projekcie:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) flutter_lints 5.0.0 (6.0.0 available) + in_app_purchase 3.2.3 + in_app_purchase_android 0.4.0+3 + in_app_purchase_platform_interface 1.4.0 + in_app_purchase_storekit 0.4.4 + json_annotation 4.9.0 lints 5.1.1 (6.0.0 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) provider 6.1.5 (6.1.5+1 available) test_api 0.7.6 (0.7.7 available) Changed 5 dependencies! 7 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Otwórz plik pubspec.yaml
i sprawdź, czy w sekcji dependencies
jest teraz wpis in_app_purchase
, a w sekcji dev_dependencies
– wpis in_app_purchase_platform_interface
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^6.0.0
cupertino_icons: ^1.0.8
firebase_auth: ^6.0.1
firebase_core: ^4.0.0
google_sign_in: ^7.1.1
http: ^1.5.0
intl: ^0.20.2
provider: ^6.1.5
logging: ^1.3.0
in_app_purchase: ^3.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
4. Konfigurowanie App Store
Aby skonfigurować zakupy w aplikacji i przetestować je na iOS, musisz utworzyć nową aplikację w App Store i utworzyć w niej produkty, które można kupić. Nie musisz niczego publikować ani wysyłać aplikacji do sprawdzenia przez Apple. Aby to zrobić, musisz mieć konto dewelopera. Jeśli nie masz konta, zarejestruj się w programie dla deweloperów Apple.
Umowy dotyczące płatnych aplikacji
Aby korzystać z zakupów w aplikacji, musisz też mieć aktywną umowę dotyczącą płatnych aplikacji w App Store Connect. Otwórz stronę https://appstoreconnect.apple.com/ i kliknij Umowy, podatki i bankowość.
Znajdziesz tu umowy dotyczące aplikacji bezpłatnych i płatnych. Stan aplikacji bezpłatnych powinien być aktywny, a stan aplikacji płatnych to „nowa”. Zapoznaj się z warunkami, zaakceptuj je i podaj wszystkie wymagane informacje.
Jeśli wszystko jest skonfigurowane prawidłowo, stan płatnych aplikacji będzie aktywny. Jest to bardzo ważne, ponieważ bez aktywnej umowy nie będziesz mieć możliwości wypróbowania zakupów w aplikacji.
Rejestrowanie identyfikatora aplikacji
Utwórz nowy identyfikator w portalu Apple Developer. Otwórz stronę developer.apple.com/account/resources/identifiers/list i kliknij ikonę „+” obok nagłówka Identyfikatory.
Wybierz identyfikatory aplikacji
Wybierz aplikację
Podaj opis i ustaw identyfikator pakietu tak, aby był zgodny z wartością ustawioną wcześniej w XCode.
Więcej wskazówek na temat tworzenia nowego identyfikatora aplikacji znajdziesz w pomocy dotyczącej konta dewelopera.
Tworzenie nowej aplikacji
Utwórz nową aplikację w App Store Connect, używając unikalnego identyfikatora pakietu.
Więcej wskazówek na temat tworzenia nowej aplikacji i zarządzania umowami znajdziesz w pomocy App Store Connect.
Aby przetestować zakupy w aplikacji, potrzebujesz użytkownika testowego w środowisku piaskownicy. Ten użytkownik testowy nie powinien być połączony z iTunes – służy on tylko do testowania zakupów w aplikacji. Nie możesz użyć adresu e-mail, który jest już używany na koncie Apple. W sekcji Użytkownicy i dostęp kliknij Sandbox, aby utworzyć nowe konto testowe lub zarządzać dotychczasowymi identyfikatorami Apple ID w środowisku testowym.
Teraz możesz skonfigurować użytkownika piaskownicy na iPhonie, klikając Ustawienia > Deweloper > Konto Apple w piaskownicy.
Konfigurowanie zakupów w aplikacji
Teraz skonfiguruj 3 produkty, które można kupić:
dash_consumable_2k
: zakup konsumpcyjny, który można kupić wiele razy i który przyznaje użytkownikowi 2000 kreski (waluta w aplikacji) za każdy zakup.dash_upgrade_3d
: niepodlegający konsumpcji zakup „ulepszenia”, którego można dokonać tylko raz i który daje użytkownikowi możliwość kliknięcia Dasha o odmiennym wyglądzie.dash_subscription_doubler
: subskrypcja, która przez cały okres jej trwania przyznaje użytkownikowi 2 razy więcej Dashów za kliknięcie.
Kliknij Zakupy w aplikacji.
Utwórz zakupy w aplikacji z określonymi identyfikatorami:
- Skonfiguruj
dash_consumable_2k
jako materiał eksploatacyjny. Użyjdash_consumable_2k
jako identyfikatora produktu. Nazwa referencyjna jest używana tylko w App Store Connect. Ustaw ją nadash consumable 2k
.Skonfiguruj dostępność. Produkt musi być dostępny w kraju użytkownika piaskownicy.
Dodaj cenę i ustaw ją na
$1.99
lub równowartość w innej walucie.Dodaj lokalizacje dla zakupu. Wywołaj zdarzenie zakupu
Spring is in the air
z opisem2000 dashes fly out
.Dodaj zrzut ekranu opinii. Treść nie ma znaczenia, dopóki produkt nie zostanie wysłany do sprawdzenia, ale jest wymagana, aby produkt był w stanie „Gotowy do przesłania”, co jest konieczne, gdy aplikacja pobiera produkty z App Store.
- Skonfiguruj
dash_upgrade_3d
jako produkt niekonsumpcyjny. Użyjdash_upgrade_3d
jako identyfikatora produktu. Ustaw nazwę odwołania nadash upgrade 3d
. Wywołaj zdarzenie zakupu3D Dash
z opisemBrings your dash back to the future
. Ustaw cenę na$0.99
. Skonfiguruj dostępność i prześlij zrzut ekranu z opinią w taki sam sposób jak w przypadkudash_consumable_2k
produktu.
- Skonfiguruj
dash_subscription_doubler
jako automatycznie odnawianą subskrypcję. Proces subskrypcji wygląda nieco inaczej. Najpierw musisz utworzyć grupę subskrypcji. Jeśli kilka subskrypcji należy do tej samej grupy, użytkownik może subskrybować tylko jedną z nich w danym momencie, ale może przejść na wyższą lub niższą wersję subskrypcji. Zadzwoń do grupysubscriptions
.Dodaj lokalizację dla grupy subskrypcji.
Następnie utworzysz subskrypcję. Ustaw nazwę odniesienia na
dash subscription doubler
, a identyfikator produktu nadash_subscription_doubler
.Następnie wybierz czas trwania subskrypcji (1 tydzień) i lokalizacje. Nazwij tę subskrypcję
Jet Engine
i dodaj opisDoubles your clicks
. Ustaw cenę na$0.49
. Skonfiguruj dostępność i prześlij zrzut ekranu z opinią w taki sam sposób jak w przypadkudash_consumable_2k
produktu.
Produkty powinny być teraz widoczne na listach:
5. Konfigurowanie Sklepu Play
Podobnie jak w przypadku App Store, w Sklepie Play też musisz mieć konto dewelopera. Jeśli jeszcze go nie masz, zarejestruj konto.
Tworzenie nowej aplikacji
Utwórz nową aplikację w Konsoli Google Play:
- Otwórz Konsolę Play.
- Kliknij Wszystkie aplikacje > Utwórz aplikację.
- Wybierz język domyślny i nazwij aplikację. Wpisz taką nazwę, jaka ma być wyświetlana w Google Play. Możesz ją później zmienić.
- Określ, że Twoja aplikacja jest grą. Możesz to później zmienić.
- Określ, czy aplikacja ma być bezpłatna czy płatna.
- Wypełnij deklaracje „Wytyczne dotyczące treści” i „Przepisy eksportowe USA”.
- Kliknij Utwórz aplikację.
Po utworzeniu aplikacji przejdź do panelu i wykonaj wszystkie zadania w sekcji Skonfiguruj aplikację. W tym miejscu podajesz informacje o aplikacji, takie jak oceny treści i zrzuty ekranu.
Podpisywanie aplikacji
Aby testować zakupy w aplikacji, musisz przesłać do Google Play co najmniej jedną wersję.
W tym celu musisz podpisać kompilację wersji czymś innym niż klucze debugowania.
Tworzenie magazynu kluczy
Jeśli masz już magazyn kluczy, przejdź do następnego kroku. Jeśli nie, utwórz go, uruchamiając to polecenie w wierszu poleceń.
Na komputerze Mac lub z Linuksem użyj tego polecenia:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
W systemie Windows użyj tego polecenia:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
To polecenie zapisuje plik key.jks
w katalogu domowym. Jeśli chcesz zapisać plik w innym miejscu, zmień argument przekazywany do parametru -keystore
. Zachowaj
keystore
plik prywatny; nie sprawdzaj go w publicznym systemie kontroli wersji.
Odwołanie do magazynu kluczy z aplikacji
Utwórz plik o nazwie <your app dir>/android/key.properties
, który zawiera odwołanie do magazynu kluczy:
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>
Konfigurowanie podpisywania w Gradle
Skonfiguruj podpisywanie aplikacji, edytując plik <your app dir>/android/app/build.gradle.kts
.
Dodaj informacje o magazynie kluczy z pliku właściwości przed blokiem android
:
import java.util.Properties
import java.io.FileInputStream
plugins {
// omitted
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Wczytaj plik key.properties
do obiektu keystoreProperties
.
Zaktualizuj blok buildTypes
, aby:
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Skonfiguruj blok signingConfigs
w pliku build.gradle.kts
modułu, podając informacje o konfiguracji podpisywania:
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Wersje aplikacji do publikacji będą teraz podpisywane automatycznie.
Więcej informacji o podpisywaniu aplikacji znajdziesz w artykule Podpisywanie aplikacji na stronie developer.android.com.
Przesyłanie pierwszej wersji
Po skonfigurowaniu aplikacji do podpisywania możesz ją skompilować, uruchamiając to polecenie:
flutter build appbundle
To polecenie domyślnie generuje wersję do wdrożenia, a dane wyjściowe można znaleźć w lokalizacji <your app dir>/build/app/outputs/bundle/release/
.
Na panelu w Konsoli Google Play kliknij Testuj i publikuj > Testowanie > Test zamknięty i utwórz nową wersję testu zamkniętego.
Następnie prześlij pakiet aplikacji app-release.aab
wygenerowany przez polecenie kompilacji.
Kliknij Zapisz, a potem Sprawdź wersję.
Na koniec kliknij Rozpocznij wdrażanie w ramach testów zamkniętych, aby aktywować wersję do testów zamkniętych.
Konfigurowanie użytkowników testowych
Aby można było testować zakupy w aplikacji, konta Google testerów muszą być dodane w Konsoli Google Play w 2 miejscach:
- na konkretną ścieżkę testu (testy wewnętrzne);
- Jako tester licencji
Najpierw dodaj testera do ścieżki testu wewnętrznego. Wróć do sekcji Testowanie i publikowanie > Testowanie > Test wewnętrzny i kliknij kartę Testerzy.
Utwórz nową listę e-mailową, klikając Utwórz listę e-mailową. Nadaj liście nazwę i dodaj adresy e-mail kont Google, które potrzebują dostępu do testowania zakupów w aplikacji.
Następnie zaznacz pole wyboru obok listy i kliknij Zapisz zmiany.
Następnie dodaj testerów licencji:
- Wróć do widoku Wszystkie aplikacje w Konsoli Google Play.
- Kliknij Ustawienia > Testowanie licencji.
- Dodaj te same adresy e-mail testerów, którzy muszą mieć możliwość testowania zakupów w aplikacji.
- Ustaw Odpowiedź licencji na
RESPOND_NORMALLY
. - Kliknij Zapisz zmiany.
Konfigurowanie zakupów w aplikacji
Teraz skonfigurujesz produkty, które można kupić w aplikacji.
Podobnie jak w App Store musisz zdefiniować 3 różne zakupy:
dash_consumable_2k
: zakup konsumpcyjny, który można kupić wiele razy i który przyznaje użytkownikowi 2000 kreski (waluta w aplikacji) za każdy zakup.dash_upgrade_3d
: jednorazowy zakup „ulepszenia”, które daje użytkownikowi możliwość klikania Dasha o odmiennym wyglądzie.dash_subscription_doubler
: subskrypcja, która przez cały okres jej trwania przyznaje użytkownikowi 2 razy więcej Dashów za kliknięcie.
Najpierw dodaj produkty konsumpcyjne i niekonsumpcyjne.
- Otwórz Konsolę Google Play i wybierz aplikację.
- Kliknij Zarabianie > Produkty > Produkty w aplikacji.
- Kliknij Utwórz produkt
.
- Wpisz wszystkie wymagane informacje o produkcie. Upewnij się, że identyfikator produktu jest dokładnie taki sam, jak identyfikator, którego chcesz użyć.
- Kliknij Zapisz.
- Kliknij Aktywuj.
- Powtórz ten proces w przypadku zakupu „ulepszenia” niepodlegającego konsumpcji.
Następnie dodaj subskrypcję:
- Otwórz Konsolę Google Play i wybierz aplikację.
- Kliknij Zarabianie > Produkty > Subskrypcje.
- Kliknij Utwórz subskrypcję
.
- Wpisz wszystkie wymagane informacje o subskrypcji. Upewnij się, że identyfikator produktu jest dokładnie taki sam, jak identyfikator, którego chcesz użyć.
- Kliknij Zapisz.
Zakupy powinny być teraz skonfigurowane w Konsoli Play.
6. Konfigurowanie Firebase
W tym module dowiesz się, jak używać usługi backendu do weryfikowania i śledzenia zakupów użytkowników.
Korzystanie z usługi backendu ma kilka zalet:
- Możesz bezpiecznie weryfikować transakcje.
- Możesz reagować na zdarzenia związane z płatnościami w sklepach z aplikacjami.
- Zakupy możesz śledzić w bazie danych.
- Użytkownicy nie będą mogli oszukać aplikacji, aby uzyskać dostęp do funkcji premium, cofając zegar systemowy.
Usługę backendu można skonfigurować na wiele sposobów, ale w tym przypadku użyjesz funkcji w chmurze i Firestore w ramach Firebase od Google.
Pisanie backendu wykracza poza zakres tego laboratorium, więc kod początkowy zawiera już projekt Firebase, który obsługuje podstawowe zakupy, aby ułatwić Ci rozpoczęcie pracy.
Aplikacja startowa zawiera też wtyczki Firebase.
Teraz musisz tylko utworzyć własny projekt Firebase, skonfigurować aplikację i backend dla Firebase, a na koniec wdrożyć backend.
Tworzenie projektu Firebase
Otwórz konsolę Firebase i utwórz nowy projekt Firebase. W tym przykładzie nazwij projekt Dash Clicker.
W aplikacji backendowej wiążesz zakupy z określonym użytkownikiem, dlatego musisz uwierzytelniać użytkowników. W tym celu użyj modułu uwierzytelniania Firebase z logowaniem przez Google.
- W panelu Firebase otwórz Uwierzytelnianie i w razie potrzeby włącz je.
- Otwórz kartę Metoda logowania i włącz dostawcę logowania Google.
Będziesz też używać bazy danych Firestore w Firebase, więc włącz również tę usługę.
Ustaw reguły Cloud Firestore w ten sposób:
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
}
}
}
Konfigurowanie Firebase w Flutterze
Zalecany sposób instalowania Firebase w aplikacji Flutter to użycie wiersza poleceń FlutterFire. Postępuj zgodnie z instrukcjami na stronie konfiguracji.
Podczas uruchamiania polecenia flutterfire configure wybierz projekt utworzony w poprzednim kroku.
$ 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>
Następnie włącz iOS i Android, wybierając te 2 platformy.
? Which platforms should your configuration support (use arrow keys & space to select)? › ✔ android ✔ ios macos web
Gdy pojawi się pytanie o zastąpienie pliku firebase_options.dart, wybierz „yes” (tak).
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Konfigurowanie Firebase na Androidzie: dalsze kroki
Na panelu Firebase otwórz Przegląd projektu, kliknij Ustawienia i wybierz kartę Ogólne.
Przewiń w dół do sekcji Twoje aplikacje i wybierz aplikację dashclicker (android).
Aby zezwolić na logowanie przez Google w trybie debugowania, musisz podać odcisk cyfrowy SHA-1 certyfikatu debugowania.
Pobieranie haszu certyfikatu podpisywania debugowania
W katalogu głównym projektu aplikacji Flutter zmień katalog na android/
, a następnie wygeneruj raport podpisywania.
cd android ./gradlew :app:signingReport
Pojawi się długa lista kluczy podpisywania. Szukasz skrótu certyfikatu debugowania, więc znajdź certyfikat, w którym właściwości Variant
i Config
mają wartość debug
. Plik keystore prawdopodobnie znajduje się w folderze domowym w katalogu .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
Skopiuj skrót SHA-1 i wypełnij ostatnie pole w oknie przesyłania aplikacji.
Na koniec ponownie uruchom flutterfire configure
, aby zaktualizować aplikację i uwzględnić konfigurację podpisywania.
$ flutterfire configure ? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes ✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
Konfigurowanie Firebase na iOS: dalsze kroki
Otwórz ios/Runner.xcworkspace
za pomocą Xcode
. Możesz też użyć wybranego środowiska IDE.
W VSCode kliknij prawym przyciskiem myszy folder ios/
, a następnie kliknij open in xcode
.
W Android Studio kliknij prawym przyciskiem myszy folder ios/
, a potem kliknij kolejno flutter
i open iOS module in Xcode
.
Aby umożliwić logowanie przez Google na urządzeniach z iOS, dodaj opcję konfiguracji CFBundleURLTypes
do plików kompilacji plist
. (Więcej informacji znajdziesz w dokumentacji google_sign_in
pakietu). W tym przypadku plik to ios/Runner/Info.plist
.
Para klucz-wartość została już dodana, ale jej wartości muszą zostać zastąpione:
- Pobierz wartość
REVERSED_CLIENT_ID
z plikuGoogleService-Info.plist
bez otaczającego ją elementu<string>..</string>
. - Zastąp wartość w pliku
ios/Runner/Info.plist
pod kluczemCFBundleURLTypes
.
<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>
Konfiguracja Firebase została zakończona.
7. Odsłuchiwanie aktualizacji dotyczących zakupów
W tej części ćwiczenia przygotujesz aplikację do zakupu produktów. Proces ten obejmuje nasłuchiwanie aktualizacji i błędów zakupu po uruchomieniu aplikacji.
Słuchanie aktualizacji dotyczących zakupów
W main.dart,
znajdź widżet MyHomePage
, który ma Scaffold
z BottomNavigationBar
zawierającym 2 strony. Ta strona tworzy też 3 Provider
dla DashCounter
, DashUpgrades,
i DashPurchases
. DashCounter
śledzi bieżącą liczbę kresek i automatycznie ją zwiększa. DashUpgrades
zarządza ulepszeniami, które możesz kupić za Dash. Ten Codelab dotyczy DashPurchases
.
Domyślnie obiekt dostawcy jest definiowany, gdy po raz pierwszy zostanie o niego wysłane żądanie. Ten obiekt nasłuchuje aktualizacji zakupu bezpośrednio po uruchomieniu aplikacji, więc wyłącz na nim leniwe wczytywanie za pomocą tego kodu: lazy: false
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
Potrzebujesz też instancji elementu InAppPurchaseConnection
. Aby jednak można było testować aplikację, musisz mieć możliwość symulowania połączenia. Aby to zrobić, utwórz metodę instancji, którą można zastąpić w teście, i dodaj ją do 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!;
}
}
Zaktualizuj test w ten sposób:
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable({
required PurchaseParam purchaseParam,
bool autoConsume = true,
}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(
ProductDetailsResponse(productDetails: [], notFoundIDs: []),
);
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
W lib/logic/dash_purchases.dart
przejdź do kodu DashPurchasesChangeNotifier
. Obecnie do kupionych Dashy możesz dodać tylko DashCounter
.
Dodaj właściwość subskrypcji strumienia _subscription
(typu StreamSubscription<List<PurchaseDetails>> _subscription;
), IAPConnection.instance,
i importy. Wynikowy kod powinien wyglądać tak:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
Słowo kluczowe late
jest dodawane do _subscription
, ponieważ _subscription
jest inicjowane w konstruktorze. Ten projekt jest domyślnie skonfigurowany jako niepusty (NNBD), co oznacza, że właściwości, które nie są zadeklarowane jako dopuszczające wartość null, muszą mieć wartość inną niż null. Kwalifikator late
umożliwia opóźnienie określenia tej wartości.
W konstruktorze pobierz strumień purchaseUpdated
i zacznij go słuchać. W przypadku metody dispose()
anuluj subskrypcję strumieniowania.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
Aplikacja otrzymuje teraz aktualizacje dotyczące zakupu, więc w następnej sekcji dokonasz zakupu.
Zanim przejdziesz dalej, uruchom testy z flutter test"
, aby sprawdzić, czy wszystko jest prawidłowo skonfigurowane.
$ flutter test 00:01 +1: All tests passed!
8. dokonywać zakupów;
W tej części laboratorium kodowania zastąpisz istniejące produkty testowe prawdziwymi produktami, które można kupić. Te produkty są wczytywane ze sklepów, wyświetlane na liście i kupowane po kliknięciu produktu.
Adapt PurchasableProduct
PurchasableProduct
wyświetla przykładowy produkt. Zaktualizuj go, aby wyświetlał rzeczywistą treść, zastępując klasę PurchasableProduct
w purchasable_product.dart
tym kodem:
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;
}
W dash_purchases.dart,
usuń zakupy testowe i zastąp je pustą listą List<PurchasableProduct> products = [];
.
Wczytaj dostępne zakupy
Aby umożliwić użytkownikowi dokonanie zakupu, wczytaj zakupy ze sklepu. Najpierw sprawdź, czy sklep jest dostępny. Gdy sklep jest niedostępny, ustawienie storeState
na notAvailable
powoduje wyświetlenie użytkownikowi komunikatu o błędzie.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Gdy sklep będzie dostępny, wczytaj dostępne zakupy. W przypadku poprzedniej konfiguracji Google Play i App Store spodziewaj się zobaczyć storeKeyConsumable
, storeKeySubscription,
i storeKeyUpgrade
. Gdy oczekiwany zakup nie jest dostępny, wydrukuj te informacje w konsoli. Możesz też wysłać je do usługi backendu.
Metoda await iapConnection.queryProductDetails(ids)
zwraca zarówno identyfikatory, których nie udało się znaleźć, jak i produkty, które można kupić. Użyj productDetails
z odpowiedzi, aby zaktualizować interfejs, i ustaw StoreState
na available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
// ...
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails
.map((e) => PurchasableProduct(e))
.toList();
storeState = StoreState.available;
notifyListeners();
}
Wywołaj funkcję loadPurchases()
w konstruktorze:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases(); // Add this line
}
Na koniec zmień wartość pola storeState
z StoreState.available
na StoreState.loading:
.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Wyświetlanie produktów, które można kupić
Weź pod uwagę plik purchase_page.dart
. Widżet PurchasePage
wyświetla _PurchasesLoading
, _PurchaseList,
lub _PurchasesNotAvailable,
w zależności od StoreState
. Widżet wyświetla też poprzednie zakupy użytkownika, które są wykorzystywane w następnym kroku.
_PurchaseList
widżet wyświetla listę produktów, które można kupić, i wysyła żądanie zakupu do obiektu 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(),
);
}
}
Jeśli produkty są prawidłowo skonfigurowane, powinny być widoczne w sklepach na Androida i iOS. Pamiętaj, że może minąć trochę czasu, zanim zakupy będą dostępne po wprowadzeniu ich do odpowiednich konsol.
Wróć do dash_purchases.dart
i wdroż funkcję zakupu produktu. Wystarczy oddzielić materiały eksploatacyjne od nieeksploatacyjnych. Ulepszenie i produkty subskrypcyjne są produktami niekonsumpcyjnymi.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails,
'${product.id} is not a known product',
);
}
}
Zanim przejdziesz dalej, utwórz zmienną _beautifiedDashUpgrade
i zaktualizuj beautifiedDash
getter, aby się do niej odwoływał.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
Metoda _onPurchaseUpdate
otrzymuje aktualizacje zakupu, aktualizuje stan produktu wyświetlanego na stronie zakupu i stosuje zakup do logiki licznika. Po obsłużeniu zakupu ważne jest wywołanie funkcji completePurchase
, aby sklep wiedział, że zakup został prawidłowo obsłużony.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. Konfigurowanie backendu
Zanim przejdziesz do śledzenia i weryfikowania zakupów, skonfiguruj backend Dart, aby to umożliwić.
W tej sekcji pracuj w folderze dart-backend/
jako folderze głównym.
Sprawdź, czy masz zainstalowane te narzędzia:
Omówienie projektu podstawowego
Niektóre części tego projektu są uważane za wykraczające poza zakres tego laboratorium, dlatego są uwzględnione w kodzie początkowym. Zanim zaczniesz, warto przejrzeć kod początkowy, aby zorientować się, jak będzie wyglądać struktura projektu.
Ten kod backendu może działać lokalnie na Twoim komputerze, więc nie musisz go wdrażać, aby go używać. Musisz jednak mieć możliwość połączenia urządzenia deweloperskiego (Androida lub iPhone’a) z komputerem, na którym będzie działać serwer. W tym celu muszą być w tej samej sieci, a Ty musisz znać adres IP swojego urządzenia.
Spróbuj uruchomić serwer za pomocą tego polecenia:
$ dart ./bin/server.dart Serving at http://0.0.0.0:8080
Backend Dart korzysta z shelf
i shelf_router
do obsługi punktów końcowych interfejsu API. Domyślnie serwer nie udostępnia żadnych tras. Później utworzysz ścieżkę do obsługi procesu weryfikacji zakupu.
Jednym z elementów, który jest już uwzględniony w kodzie początkowym, jest IapRepository
w lib/iap_repository.dart
. Ponieważ nauka interakcji z Firestore lub bazami danych w ogóle nie jest uważana za istotną w tym laboratorium, kod początkowy zawiera funkcje tworzenia i aktualizowania zakupów w Firestore, a także wszystkie klasy tych zakupów.
Konfigurowanie dostępu do Firebase
Aby uzyskać dostęp do Firebase Firestore, potrzebujesz klucza dostępu do konta usługi. Wygeneruj go, otwierając ustawienia projektu Firebase i przechodząc do sekcji Konta usługi, a następnie wybierając Wygeneruj nowy klucz prywatny.
Skopiuj pobrany plik JSON do folderu assets/
i zmień jego nazwę na service-account-firebase.json
.
Konfigurowanie dostępu do Google Play
Aby uzyskać dostęp do Sklepu Play w celu weryfikacji zakupów, musisz wygenerować konto usługi z tymi uprawnieniami i pobrać dla niego dane logowania w formacie JSON.
- Otwórz stronę interfejsu Google Play Android Developer API w Google Cloud Console.
Jeśli Konsola Google Play poprosi Cię o utworzenie projektu lub połączenie go z istniejącym projektem, najpierw wykonaj tę czynność, a potem wróć na tę stronę.
- Następnie otwórz stronę Konta usługi i kliknij + Utwórz konto usługi.
- Wpisz nazwę konta usługi i kliknij Utwórz i kontynuuj.
- Wybierz rolę Subskrybent Pub/Sub i kliknij Gotowe.
- Po utworzeniu konta kliknij Zarządzaj kluczami.
- Kliknij Dodaj klucz > Utwórz nowy klucz.
- Utwórz i pobierz klucz JSON.
- Zmień nazwę pobranego pliku na
service-account-google-play.json,
i przenieś go do kataloguassets/
. - Następnie otwórz stronę Użytkownicy i uprawnienia w Konsoli Play
- Kliknij Zaproś nowych użytkowników i wpisz adres e-mail utworzonego wcześniej konta usługi. Adres e-mail znajdziesz w tabeli na stronie Konta usługi
- Przyznaj aplikacji uprawnienia Wyświetlanie danych finansowych i Zarządzanie zamówieniami i subskrypcjami.
- Kliknij Zaproś użytkownika.
Musimy jeszcze otworzyć plik lib/constants.dart,
i zastąpić wartość androidPackageId
identyfikatorem pakietu wybranym dla aplikacji na Androida.
Konfigurowanie dostępu do Apple App Store
Aby uzyskać dostęp do App Store w celu weryfikacji zakupów, musisz skonfigurować wspólny klucz tajny:
- Otwórz App Store Connect.
- Otwórz Moje aplikacje i wybierz aplikację.
- W menu bocznym kliknij Ogólne > Informacje o aplikacji.
- W sekcji App-Specific Shared Secret (Wspólny tajny klucz aplikacji) kliknij Manage (Zarządzaj).
- Wygeneruj nowy klucz tajny i go skopiuj.
- Otwórz plik
lib/constants.dart,
i zastąp wartośćappStoreSharedSecret
wygenerowanym przed chwilą tajnym kluczem.
Plik konfiguracji stałych
Zanim przejdziesz dalej, sprawdź, czy w pliku lib/constants.dart
skonfigurowano te stałe:
androidPackageId
: identyfikator pakietu używany na Androidzie, np.com.example.dashclicker
appStoreSharedSecret
: tajny klucz umożliwiający dostęp do App Store Connect w celu weryfikacji zakupu.bundleId
: identyfikator pakietu używany w iOS, np.com.example.dashclicker
Na razie możesz zignorować pozostałe stałe.
10. Weryfikowanie zakupów
Ogólny proces weryfikacji zakupów jest podobny w przypadku iOS i Androida.
W przypadku obu sklepów aplikacja otrzymuje token po dokonaniu zakupu.
Ten token jest wysyłany przez aplikację do usługi backendu, która następnie weryfikuje zakup na serwerach odpowiedniego sklepu za pomocą podanego tokena.
Usługa backendu może wtedy zapisać zakup i odpowiedzieć aplikacji, czy jest on prawidłowy.
Dzięki temu, że weryfikacja jest przeprowadzana przez usługę backendu w sklepach, a nie przez aplikację działającą na urządzeniu użytkownika, możesz zapobiec uzyskaniu przez użytkownika dostępu do funkcji premium, np. przez cofnięcie zegara systemowego.
Konfigurowanie po stronie Fluttera
Konfigurowanie uwierzytelniania
Ponieważ zakupy będą wysyłane do usługi backendu, musisz mieć pewność, że użytkownik jest uwierzytelniony podczas dokonywania zakupu. Większość logiki uwierzytelniania jest już dodana w projekcie początkowym. Musisz tylko sprawdzić, czy PurchasePage
wyświetla przycisk logowania, gdy użytkownik nie jest jeszcze zalogowany. Dodaj ten kod na początku metody kompilacji pliku PurchasePage
:
lib/pages/purchase_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart'; // Add this import
import '../model/firebase_state.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart'; // And this one as well
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) { // Update from here
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
} // To here.
// ...
Wywoływanie punktu końcowego weryfikacji połączeń z aplikacji
W aplikacji utwórz funkcję _verifyPurchase(PurchaseDetails purchaseDetails)
, która wywołuje punkt końcowy /verifypurchase
na backendzie Dart za pomocą wywołania http post.
Wyślij wybrany sklep (google_play
w przypadku Sklepu Play lub app_store
w przypadku App Store), serverVerificationData
i productID
. Serwer zwraca kod stanu wskazujący, czy zakup został zweryfikowany.
W stałych aplikacji skonfiguruj adres IP serwera na adres IP komputera lokalnego.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'dart:convert'; // Add this import
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // And this import
import 'package:in_app_purchase/in_app_purchase.dart';
import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart'; // And this one
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier; // Add this line
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter, this.firebaseNotifier) { // Update this line
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Dodaj firebaseNotifier
z utworzeniem DashPurchases
w main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Dodaj funkcję pobierającą użytkownika w klasie FirebaseNotifier, aby przekazywać identyfikator użytkownika do funkcji weryfikacji zakupu.
lib/logic/firebase_notifier.dart
Future<FirebaseFirestore> get firestore async {
var isInitialized = await _isInitialized.future;
if (!isInitialized) {
throw Exception('Firebase is not initialized');
}
return FirebaseFirestore.instance;
}
User? get user => FirebaseAuth.instance.currentUser; // Add this line
Future<void> load() async {
// ...
Dodaj funkcję _verifyPurchase
do klasy DashPurchases
. Ta funkcja async
zwraca wartość logiczną wskazującą, czy zakup został zweryfikowany.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
Wywołaj funkcję _verifyPurchase
w _handlePurchase
tuż przed zastosowaniem zakupu. Zastosuj zakup dopiero po jego zweryfikowaniu. W aplikacji produkcyjnej możesz to dodatkowo określić, np. zastosować subskrypcję próbną, gdy sklep jest tymczasowo niedostępny. W tym przykładzie zastosuj jednak zakup, gdy zostanie on zweryfikowany.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
W aplikacji wszystko jest już gotowe do weryfikacji zakupów.
Konfigurowanie usługi backendu
Następnie skonfiguruj backend do weryfikowania zakupów.
Tworzenie modułów obsługi zakupu
Proces weryfikacji w przypadku obu sklepów jest niemal identyczny, dlatego skonfiguruj abstrakcyjną klasę PurchaseHandler
z oddzielnymi implementacjami dla każdego sklepu.
Zacznij od dodania pliku purchase_handler.dart
do folderu lib/
, w którym zdefiniujesz abstrakcyjną klasę PurchaseHandler
z 2 metodami abstrakcyjnymi do weryfikacji 2 rodzajów zakupów: subskrypcji i zakupów bez subskrypcji.
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,
});
}
Jak widać, każda metoda wymaga 3 parametrów:
userId:
Identyfikator zalogowanego użytkownika, dzięki czemu możesz powiązać zakupy z użytkownikiem.productData:
Dane o produkcie. Za chwilę to zdefiniujesz.token:
Token przekazany użytkownikowi przez sklep.
Aby ułatwić korzystanie z tych funkcji obsługi zakupu, dodaj metodę verifyPurchase()
, której można używać zarówno w przypadku subskrypcji, jak i zakupów jednorazowych:
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,
);
}
}
Teraz w obu przypadkach możesz po prostu wywołać funkcję verifyPurchase
, ale nadal mieć osobne implementacje.
Klasa ProductData
zawiera podstawowe informacje o różnych produktach, które można kupić, w tym identyfikator produktu (czasami nazywany też SKU) i ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
może być subskrypcją lub nie.
lib/products.dart
enum ProductType { subscription, nonSubscription }
Na koniec lista produktów jest definiowana jako mapa w tym samym pliku.
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,
),
};
Następnie zdefiniuj implementacje zastępcze dla Sklepu Google Play i Apple App Store. Zacznij od Google Play:
Utwórz lib/google_play_purchase_handler.dart
i dodaj klasę, która rozszerza właśnie napisaną klasę 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;
}
}
Na razie zwraca true
w przypadku metod obsługi. Wrócimy do nich później.
Jak być może zauważysz, konstruktor przyjmuje instancję klasy IapRepository
. Obsługa zakupu używa tej instancji do późniejszego przechowywania informacji o zakupach w Firestore. Aby komunikować się z Google Play, używasz podanego AndroidPublisherApi
.
Następnie wykonaj te same czynności w przypadku modułu obsługi sklepu z aplikacjami. Utwórz lib/app_store_purchase_handler.dart
i ponownie dodaj klasę, która rozszerza PurchaseHandler
:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(this.iapRepository);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Świetnie. Masz teraz 2 procedury obsługi zakupu. Następnie utwórz punkt końcowy interfejsu API weryfikacji zakupu.
Używanie modułów obsługi zakupu
Otwórz bin/server.dart
i utwórz punkt końcowy API za pomocą shelf_route
:
bin/server.dart
import 'dart:convert';
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
if (payload case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
Kod wykonuje te działania:
- Zdefiniuj punkt końcowy POST, który będzie wywoływany z utworzonej wcześniej aplikacji.
- Zdekoduj ładunek JSON i wyodrębnij te informacje:
userId
: identyfikator zalogowanego użytkownikasource
: używany sklep,app_store
lubgoogle_play
.productData
: pobrano z utworzonego wcześniejproductDataMap
.token
: zawiera dane weryfikacyjne do wysłania do sklepów.
- Wywołaj metodę
verifyPurchase
dlaGooglePlayPurchaseHandler
lubAppStorePurchaseHandler
w zależności od źródła. - Jeśli weryfikacja się powiedzie, metoda zwraca klientowi wartość
Response.ok
. - Jeśli weryfikacja się nie powiedzie, metoda zwraca klientowi wartość
Response.internalServerError
.
Po utworzeniu punktu końcowego interfejsu API musisz skonfigurować 2 procedury obsługi zakupu. Wymaga to wczytania kluczy konta usługi uzyskanych w poprzednim kroku i skonfigurowania dostępu do różnych usług, w tym interfejsu Android Publisher API i interfejsu Firebase Firestore API. Następnie utwórz 2 funkcje obsługi zakupu z różnymi zależnościami:
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Weryfikowanie zakupów na Androidzie: wdrażanie procedury obsługi zakupów
Następnie kontynuuj implementację modułu obsługi zakupu w Google Play.
Google udostępnia już pakiety Dart do interakcji z interfejsami API, których potrzebujesz do weryfikacji zakupów. Zostały one zainicjowane w pliku server.dart
, a teraz są używane w klasie GooglePlayPurchaseHandler
.
Zaimplementuj moduł obsługi zakupów, które nie są subskrypcjami:
lib/google_play_purchase_handler.dart
/// Handle non-subscription purchases (one time purchases).
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
W podobny sposób możesz zaktualizować moduł obsługi zakupu subskrypcji:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
Dodaj tę metodę, aby ułatwić analizowanie identyfikatorów zamówień, oraz 2 metody analizowania stanu zakupu.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
Zakupy w Google Play powinny zostać zweryfikowane i zapisane w bazie danych.
Następnie przejdź do zakupów w App Store na urządzeniach z iOS.
Weryfikowanie zakupów w iOS: wdrażanie modułu obsługi zakupów
Do weryfikacji zakupów w App Store służy pakiet Dart innej firmy o nazwie app_store_server_sdk
, który ułatwia ten proces.
Zacznij od utworzenia instancji ITunesApi
. Użyj konfiguracji piaskownicy i włącz logowanie, aby ułatwić debugowanie błędów.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(ITunesEnvironment.sandbox(), loggingEnabled: true),
);
W przeciwieństwie do interfejsów API Google Play App Store używa teraz tych samych punktów końcowych interfejsu API zarówno w przypadku subskrypcji, jak i produktów nieobjętych subskrypcją. Oznacza to, że możesz używać tej samej logiki w przypadku obu tych funkcji. Połącz je, aby wywoływały tę samą implementację:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
// See next step
}
Teraz zaimplementuj 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;
}
}
Zakupy w App Store powinny być teraz zweryfikowane i przechowywane w bazie danych.
Uruchamianie backendu
W tym momencie możesz uruchomić polecenie dart bin/server.dart
, aby udostępnić punkt końcowy /verifypurchase
.
$ dart bin/server.dart Serving at http://0.0.0.0:8080
11. Monitorowanie zakupów
Zalecany sposób śledzenia zakupów użytkowników to usługa backendu. Dzieje się tak, ponieważ backend może odpowiadać na zdarzenia ze sklepu, a tym samym jest mniej podatny na nieaktualne informacje z powodu buforowania i mniej podatny na manipulacje.
Najpierw skonfiguruj przetwarzanie zdarzeń w sklepie na backendzie za pomocą backendu Dart, który tworzysz.
Przetwarzanie zdarzeń w sklepie na backendzie
Sklepy mogą informować Twój backend o wszelkich zdarzeniach związanych z płatnościami, np. o odnowieniu subskrypcji. Możesz przetwarzać te zdarzenia na backendzie, aby zakupy w bazie danych były aktualne. W tej sekcji skonfiguruj to ustawienie zarówno w Sklepie Google Play, jak i w Apple App Store.
Przetwarzanie zdarzeń rozliczeniowych w Google Play
Google Play udostępnia zdarzenia rozliczeniowe za pomocą tematu Cloud Pub/Sub. Są to w zasadzie kolejki wiadomości, do których można publikować wiadomości i z których można je pobierać.
Ponieważ jest to funkcja specyficzna dla Google Play, należy ją umieścić w sekcji GooglePlayPurchaseHandler
.
Zacznij od otwarcia pliku lib/google_play_purchase_handler.dart
i dodania importu PubsubApi
:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Następnie przekaż PubsubApi
do GooglePlayPurchaseHandler
i zmodyfikuj konstruktor klasy, aby utworzyć Timer
w ten sposób:
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();
});
}
Usługa Timer
jest skonfigurowana tak, aby wywoływać metodę _pullMessageFromPubSub
co 10 sekund. Możesz dostosować czas trwania do własnych preferencji.
Następnie utwórz_pullMessageFromPubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(maxMessages: 1000);
final topicName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(ackIds: [id]);
final subscriptionName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
Dodany właśnie kod komunikuje się z tematem Pub/Sub w Google Cloud co 10 sekund i prosi o nowe wiadomości. Następnie przetwarza każdą wiadomość metodą _processMessage
.
Ta metoda dekoduje przychodzące wiadomości i uzyskuje zaktualizowane informacje o każdym zakupie, zarówno subskrypcji, jak i produktów nieobjętych subskrypcją, w razie potrzeby wywołując istniejącą funkcję handleSubscription
lub handleNonSubscription
.
Każda wiadomość musi zostać potwierdzona za pomocą metody _askMessage
.
Następnie dodaj wymagane zależności do pliku server.dart
. Dodaj PubsubApi.cloudPlatformScope do konfiguracji danych logowania:
bin/server.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub; // Add this import
final clientGooglePlay = await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // Add this line
]);
Następnie utwórz instancję PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Na koniec przekaż go do konstruktora GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // Add this line
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Konfigurowanie Google Play
Masz już kod do wykorzystywania zdarzeń związanych z płatnościami z tematu Pub/Sub, ale nie masz jeszcze utworzonego tematu Pub/Sub ani nie publikujesz żadnych zdarzeń związanych z płatnościami. Czas to skonfigurować.
Najpierw utwórz temat Pub/Sub:
- Ustaw wartość
googleCloudProjectId
wconstants.dart
na identyfikator Twojego projektu Google Cloud. - Otwórz stronę Cloud Pub/Sub w konsoli Google Cloud.
- Upewnij się, że jesteś w projekcie Firebase, i kliknij + Utwórz temat.
- Nadaj nowemu tematowi nazwę identyczną z wartością ustawioną dla parametru
googlePlayPubsubBillingTopic
w plikuconstants.dart
. W takim przypadku nadaj mu nazwęplay_billing
. Jeśli wybierzesz coś innego, pamiętaj o zaktualizowaniuconstants.dart
. Utwórz temat. - Na liście tematów Pub/Sub kliknij 3 pionowe kropki przy utworzonym przez Ciebie temacie i kliknij Wyświetl uprawnienia.
- Na pasku bocznym po prawej stronie kliknij Dodaj podmiot.
- Dodaj tutaj
google-play-developer-notifications@system.gserviceaccount.com
i przypisz mu rolę publikującego w Pub/Sub. - Zapisz zmiany uprawnień.
- Skopiuj nazwę tematu, który właśnie został utworzony.
- Ponownie otwórz Konsolę Play i wybierz aplikację z listy Wszystkie aplikacje.
- Przewiń w dół i kliknij Zarabianie > Konfiguracja ustawień zarabiania.
- Wpisz pełny temat i zapisz zmiany.
Wszystkie zdarzenia związane z płatnościami w Google Play będą teraz publikowane w tym temacie.
Przetwarzanie zdarzeń związanych z płatnościami w App Store
Następnie wykonaj te same czynności w przypadku zdarzeń rozliczeniowych w App Store. Istnieją 2 skuteczne sposoby implementowania obsługi aktualizacji w przypadku zakupów w App Store. Jednym z nich jest wdrożenie elementu webhook, który udostępniasz Apple i którego firma używa do komunikacji z Twoim serwerem. Drugi sposób, który znajdziesz w tych ćwiczeniach z programowania, polega na połączeniu się z interfejsem App Store Server API i ręcznym uzyskaniu informacji o subskrypcji.
Ten codelab skupia się na drugim rozwiązaniu, ponieważ w przypadku webhooka musisz udostępnić serwer w internecie.
W środowisku produkcyjnym najlepiej mieć oba te elementy. Webhook do pobierania zdarzeń z App Store oraz interfejs Server API, jeśli przegapisz zdarzenie lub musisz ponownie sprawdzić stan subskrypcji.
Zacznij od otwarcia pliku lib/app_store_purchase_handler.dart
i dodania zależności AppStoreServerAPI
:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI; // Add this member
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // And this parameter
);
Zmodyfikuj konstruktor, aby dodać licznik czasu, który będzie wywoływać metodę _pullStatus
. Ten licznik będzie wywoływać metodę _pullStatus
co 10 sekund. Możesz dostosować czas trwania tego timera do swoich potrzeb.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Następnie utwórz metodę _pullStatus
w ten sposób:
lib/app_store_purchase_handler.dart
/// Request the App Store for the latest subscription status.
/// Updates all App Store subscriptions in the database.
/// NOTE: This code only handles when a subscription expires as example.
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where(
(element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore,
);
for (final purchase in appStoreSubscriptions) {
final status = await appStoreServerAPI.getAllSubscriptionStatuses(
purchase.orderId,
);
// Obtain all subscriptions for the order ID.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0,
);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(
SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate,
),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
),
);
}
}
}
}
Ta metoda działa w ten sposób:
- Pobiera listę aktywnych subskrypcji z Firestore za pomocą IapRepository.
- W przypadku każdego zamówienia wysyła żądanie stanu subskrypcji do interfejsu App Store Server API.
- Pobiera ostatnią transakcję zakupu subskrypcji.
- sprawdza datę ważności,
- Aktualizuje stan subskrypcji w Firestore. Jeśli subskrypcja wygasła, zostanie oznaczona jako wygasła.
Na koniec dodaj cały niezbędny kod, aby skonfigurować dostęp do interfejsu App Store Server API:
bin/server.dart
import 'package:app_store_server_sdk/app_store_server_sdk.dart'; // Add this import
import 'package:firebase_backend_dart/constants.dart'; // And this one.
// add from here
final subscriptionKeyAppStore = File(
'assets/SubscriptionKey.p8',
).readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // Add this argument
),
};
Konfiguracja App Store
Następnie skonfiguruj App Store:
- Zaloguj się w App Store Connect i wybierz Użytkownicy i dostęp.
- Kliknij Integrations > Keys > In-App Purchase (Integracje > Klucze > Zakup w aplikacji).
- Aby dodać nowy, kliknij ikonę „+”.
- Nadaj mu nazwę, np. „Klucz Codelab”.
- Pobierz plik p8 zawierający klucz.
- Skopiuj go do folderu zasobów pod nazwą
SubscriptionKey.p8
. - Skopiuj identyfikator klucza z nowo utworzonego klucza i ustaw go jako stałą
appStoreKeyId
w plikulib/constants.dart
. - Skopiuj identyfikator wydawcy z góry listy kluczy i ustaw go jako stałą
appStoreIssuerId
w plikulib/constants.dart
.
Śledzenie zakupów na urządzeniu
Najbezpieczniejszym sposobem śledzenia zakupów jest śledzenie po stronie serwera, ponieważ zabezpieczenie klienta jest trudne. Musisz jednak mieć możliwość przekazywania informacji z powrotem do klienta, aby aplikacja mogła reagować na informacje o stanie subskrypcji. Przechowując zakupy w Firestore, możesz synchronizować dane z klientem i automatycznie je aktualizować.
W aplikacji jest już uwzględniony element IAPRepo, czyli repozytorium Firestore, które zawiera wszystkie dane o zakupach użytkownika w List<PastPurchase> purchases
. W repozytorium znajduje się też hasActiveSubscription,
, co oznacza, że zakup productId storeKeySubscription
ma stan, który nie wygasł. Gdy użytkownik nie jest zalogowany, lista jest pusta.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any(
(element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired,
);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
Cała logika zakupu znajduje się w klasie DashPurchases
. To w niej należy stosować lub usuwać subskrypcje. Dodaj więc iapRepo
jako właściwość w klasie i przypisz iapRepo
w konstruktorze. Następnie dodaj bezpośrednio słuchacza w konstruktorze i usuń go w metodzie dispose()
. Początkowo detektor może być pustą funkcją. Ponieważ IAPRepo
jest ChangeNotifier
, a funkcja notifyListeners()
jest wywoływana za każdym razem, gdy zmieniają się zakupy w Firestore, metoda purchasesUpdate()
jest zawsze wywoływana, gdy zmieniają się zakupione produkty.
lib/logic/dash_purchases.dart
import '../repo/iap_repo.dart'; // Add this import
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier;
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
IAPRepo iapRepo; // Add this line
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
// Add this.iapRepo as a parameter
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
Future<void> loadPurchases() async {
// Elided.
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate); // Add this line
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
Następnie przekaż IAPRepo
do konstruktora w main.dart.
. Repozytorium możesz uzyskać za pomocą context.read
, ponieważ zostało już utworzone w Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(), // Add this line
),
lazy: false,
),
Następnie napisz kod funkcji purchaseUpdate()
. W metodach dash_counter.dart,
i applyPaidMultiplier
ustaw odpowiednio mnożnik na 10 lub 1, aby nie trzeba było sprawdzać, czy subskrypcja została już zastosowana.removePaidMultiplier
Gdy stan subskrypcji się zmieni, zaktualizuj też stan produktu, który można kupić, aby na stronie zakupu wyświetlać informację, że jest on już aktywny. Ustaw właściwość _beautifiedDashUpgrade
w zależności od tego, czy uaktualnienie zostało kupione.
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();
}
}
Dzięki temu stan subskrypcji i ulepszenia będzie zawsze aktualny w usłudze backendu i zsynchronizowany z aplikacją. Aplikacja będzie odpowiednio reagować i stosować funkcje subskrypcji i ulepszenia w grze Dash clicker.
12. Wszystko gotowe
Gratulacje!!! To już koniec tego laboratorium. Gotowy kod do tego ćwiczenia znajdziesz w folderze complete.
Aby dowiedzieć się więcej, wypróbuj inne codelaby Fluttera.