1. Wprowadzenie
Dodanie zakupów w aplikacji do aplikacji Flutter wymaga prawidłowego skonfigurowania Sklepu Google Play i App Store, potwierdzenia zakupu oraz przyznania niezbędnych uprawnień, takich jak korzyści z subskrypcji.
W tym ćwiczeniu dodasz do aplikacji (dostarczonej przez nas) 3 typy zakupów w aplikacji i zweryfikujesz te zakupy za pomocą backendu Dart z Firebase. Podana aplikacja Dash Clicker zawiera grę, w której maskotka Dash jest używana jako waluta. Dodaj te opcje zakupu:
- możliwość wielokrotnego zakupu 2000 Dashes naraz.
- jednorazowy zakup licencji, która umożliwia przekształcenie starego Dash w nowoczesny Dash;
- Subskrypcja, która podwaja liczbę automatycznie generowanych kliknięć.
Pierwsza opcja zakupu daje użytkownikowi bezpośrednią korzyść w postaci 2000 Dashes. Są one dostępne bezpośrednio dla użytkownika i można je kupić wielokrotnie. Jest to tak zwany zasób jednorazowego użytku, ponieważ jest on bezpośrednio wykorzystywany i może być wykorzystany wielokrotnie.
Druga opcja umożliwia ulepszenie wyglądu Dash. Wystarczy kupić go raz, a będzie dostępny na zawsze. Taki zakup jest nazywany niekonsumpcyjnym, ponieważ nie może być wykorzystany przez aplikację, ale jest ważny na zawsze.
Trzecią i ostatnią opcją zakupu jest subskrypcja. Podczas trwania subskrypcji użytkownik będzie otrzymywać Dashe szybciej, ale gdy przestanie płacić za subskrypcję, korzyści również znikną.
Usługa backendowa (również udostępniana) działa jako aplikacja Dart, weryfikuje zakupy i przechowuje je w Firestore. Firestore ułatwia ten proces, ale w wersji produkcyjnej aplikacji możesz użyć dowolnego typu usługi backendowej.
Co utworzysz
- rozszerzysz aplikację, aby obsługiwała zakupy i subskrypcje jednorazowych produktów;
- Musisz też rozszerzyć aplikację backendową Dart, aby weryfikować i przechowywać zakupione produkty.
Czego się nauczysz
- Jak skonfigurować App Store i Sklep Play w celu sprzedaży produktów.
- Jak komunikować się ze sklepami, aby weryfikować zakupy i przechowywać je w Firestore.
- Jak zarządzać zakupami w aplikacji.
Czego potrzebujesz
- Android Studio 4.1 lub nowsza wersja
- Xcode 12 lub nowsza wersja (do tworzenia aplikacji na iOS)
- Pakiet SDK Flutter
2. Konfigurowanie środowiska programistycznego
Aby rozpocząć pracę z tym Codelab, pobierz kod i zmień identyfikator pakietu na iOS oraz nazwę pakietu na Androida.
Pobieranie kodu
Aby skopiować 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 jest klonowany do katalogu flutter-codelabs
, który zawiera kod kolekcji zajęć. Kod tego ćwiczenia z programowania jest w języku flutter-codelabs/in_app_purchases
.
Struktura katalogu w folderze flutter-codelabs/in_app_purchases
zawiera serię zrzutów ekranu pokazujących, gdzie powinieneś się znaleźć na końcu każdego kroku. Kod startowy znajduje się na kroku 0, więc znalezienie pasujących plików jest bardzo proste:
cd flutter-codelabs/in_app_purchases/step_00
Jeśli chcesz przejść do następnego kroku lub sprawdzić, jak coś powinno wyglądać po wykonaniu danego kroku, zajrzyj do katalogu o odpowiedniej nazwie. Kod ostatniego kroku znajduje się w folderze complete
.
Konfigurowanie projektu startowego
Otwórz projekt startowy step_00
w ulubionym środowisku IDE. Do zrobienia zrzutów ekranu użyliśmy Android Studio, ale Visual Studio Code też się sprawdzi. W obu edytorach upewnij się, że masz zainstalowane najnowsze wtyczki Dart i Flutter.
Aplikacje, które zamierzasz tworzyć, 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 App Store na iOS jest to identyfikator pakietu, a w Sklepie Play na Androida – identyfikator aplikacji. Te identyfikatory są zwykle tworzone za pomocą odwrotnej notacji nazwy domeny. Jeśli np. dokonujesz zakupu w aplikacji w przypadku flutter.dev, musisz użyć dev.flutter.inapppurchase
. Wymyśl identyfikator aplikacji, który ustawisz w ustawieniach projektu.
Najpierw skonfiguruj identyfikator pakietu na iOS.
Gdy projekt jest otwarty w Android Studio, kliknij prawym przyciskiem myszy folder iOS, wybierz Flutter i otwórz moduł w aplikacji Xcode.
W strukturze folderów Xcode na górze znajduje się projekt Runner, a pod nim cele Flutter, Runner i Produkty. Kliknij dwukrotnie Runner, aby edytować ustawienia projektu, a potem kliknij Podpisywanie i możliwości. Aby ustawić zespół, wpisz w polu Zespół identyfikator pakietu, który został przez Ciebie wybrany.
Możesz teraz zamknąć Xcode i wrócić do Android Studio, aby dokończyć konfigurację Androida. Aby to zrobić, otwórz plik build.gradle
w folderze android/app,
i zamień wartość applicationId
(na linii 37 na poniższym zrzucie ekranu) na identyfikator aplikacji, który jest taki sam jak identyfikator pakietu na iOS. Pamiętaj, że identyfikatory dla sklepów na iOS i Androida nie muszą być identyczne, ale jeśli będą takie same, zmniejszy to ryzyko wystąpienia błędów. Dlatego w tym CodeLab użyjemy identycznych identyfikatorów.
3. Instalowanie wtyczki
W tej części ćwiczenia zainstalujesz wtyczkę in_app_purchase.
Dodawanie zależności w pubspec
Dodaj in_app_purchase
do pliku pubspec, dodając in_app_purchase
do zależności w tym pliku:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Otwórz plik pubspec.yaml
i sprawdź, czy w sekcji dependencies
znajduje się teraz wpis in_app_purchase
, a w sekcji dev_dependencies
– in_app_purchase_platform_interface
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
Aby pobrać pakiet, kliknij pub get lub uruchom flutter pub get
w wierszu polecenia.
4. Konfigurowanie App Store
Aby skonfigurować zakupy w aplikacji i je przetestować na iOS, musisz utworzyć nową aplikację w App Store i umieścić w niej produkty do kupienia. Nie musisz niczego publikować ani wysyłać aplikacji do Apple do sprawdzenia. Aby to zrobić, musisz mieć konto dewelopera. Jeśli go nie masz, 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. Wejdź na https://appstoreconnect.apple.com/ i kliknij Agreements, Tax, and Banking (Umowy, podatki i bankowość).
Tutaj znajdziesz umowy dotyczące bezpłatnych i płatnych aplikacji. Stan aplikacji bezpłatnych powinien być aktywny, a stan aplikacji płatnych – nowy. Zapoznaj się z warunkami, zaakceptuj je i podaj wszystkie wymagane informacje.
Gdy wszystko będzie skonfigurowane prawidłowo, stan aplikacji płatnych będzie aktywny. Jest to bardzo ważne, ponieważ bez aktywnej umowy nie będzie można testować zakupów w aplikacji.
Rejestrowanie identyfikatora aplikacji
Utwórz nowy identyfikator na portalu dewelopera Apple.
Wybieranie identyfikatorów aplikacji
Wybierz aplikację
Podaj opis i ustaw identyfikator pakietu na taką samą wartość jak w XCode.
Więcej wskazówek na temat tworzenia nowego identyfikatora aplikacji znajdziesz w Centrum pomocy dotyczącego konta dewelopera .
Tworzenie nowej aplikacji
Utwórz nową aplikację w usłudze App Store Connect, korzystając z wyjątkowego 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 sandboksie. Ten użytkownik testowy nie powinien być połączony z iTunes – służy 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 Testerzy w sekcji Środowisko testowe, aby utworzyć nowe konto środowiska testowego lub zarządzać istniejącymi identyfikatorami Apple ID środowiska testowego.
Teraz możesz skonfigurować użytkownika piaskownicy na iPhonie, klikając Ustawienia > Sklep Google Play > Konto piaskownicy.
Konfigurowanie zakupów w aplikacji
Teraz skonfiguruj 3 produkty do kupienia:
dash_consumable_2k
: zakup produktu konsumpcyjnego, który można wielokrotnie kupić, co daje użytkownikowi 2000 Dashes (waluta w aplikacji) za każdy zakup.dash_upgrade_3d
: niewykorzystywane „ulepszenie” do kupienia tylko raz, które daje użytkownikowi inny wygląd Dash.dash_subscription_doubler
: subskrypcja, która przyznaje użytkownikowi dwukrotnie więcej kresek na kliknięcie przez cały okres subskrypcji.
Kliknij Zakupy w aplikacji > Zarządzaj.
Tworzenie zakupów w aplikacji za pomocą określonych identyfikatorów:
- Skonfiguruj
dash_consumable_2k
jako produkt jednorazowy.
Jako identyfikator produktu użyj wartości dash_consumable_2k
. Nazwa referencyjna jest używana tylko w usłudze App Store Connect. Ustaw ją na dash consumable 2k
i dodaj lokalizację dla zakupu. Nazwij zakup Spring is in the air
, a jako opis podaj 2000 dashes fly out
.
- Skonfiguruj
dash_upgrade_3d
jako niekonsumowalny.
Jako identyfikator produktu użyj wartości dash_upgrade_3d
. Ustaw nazwę referencyjną na dash upgrade 3d
i dodaj lokalizację dla zakupu. Nazwij zakup 3D Dash
, a jako opis podaj Brings your dash back to the future
.
- Skonfiguruj
dash_subscription_doubler
jako automatycznie odnawianą subskrypcję.
Proces subskrypcji wygląda nieco inaczej. Najpierw musisz ustawić nazwę referencyjną i identyfikator produktu:
Następnie musisz utworzyć grupę subskrypcji. Gdy wiele subskrypcji należy do tej samej grupy, użytkownik może subskrybować tylko jedną z nich w tym samym czasie, ale może łatwo przejść na wyższą lub niższą wersję subskrypcji. Wystarczy zadzwonić do tej grupy subscriptions
.
Następnie wpisz okres subskrypcji i lokalizacje. Nazwij tę subskrypcję Jet Engine
i nadaj jej opis Doubles your clicks
. Kliknij Zapisz.
Po kliknięciu przycisku Zapisz dodaj cenę subskrypcji. Wybierz dowolną cenę.
Na liście zakupów powinny teraz być widoczne 3 zakupy:
5. Konfigurowanie Sklepu Play
Podobnie jak w App Store, w Sklepie Play też będziesz potrzebować konta 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ć.
- Potwierdź, że Twoja aplikacja jest grą. Możesz to później zmienić.
- Określ, czy aplikacja ma być bezpłatna czy płatna.
- Dodaj adres e-mail, pod którym użytkownicy Sklepu Play będą mogli kontaktować się z Tobą w sprawie tej aplikacji.
- Wypełnij deklarację „Wytyczne dotyczące treści” i „Przepisy eksportowe USA”.
- Kliknij Utwórz aplikację.
Po utworzeniu aplikacji otwórz panel i wykonaj wszystkie zadania w sekcji Konfigurowanie aplikacji. Tutaj podajesz informacje o swojej aplikacji, takie jak oceny treści i zrzuty ekranu.
Podpisywanie aplikacji
Aby móc testować zakupy w aplikacji, musisz przesłać do Google Play co najmniej 1 kompilację.
W tym celu kompilacja wersji produkcyjnej musi być podpisana za pomocą czegoś innego niż klucze debugowania.
Tworzenie magazynu kluczy
Jeśli masz już repozytorium kluczy, przejdź do następnego kroku. Jeśli nie, utwórz konto, uruchamiając to polecenie w wierszu poleceń.
Na Macu lub Linuksie 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 przechowywać plik w innym miejscu, zmień argument przekazywany do parametru -keystore
. Zachowaj
keystore
plik prywatny; nie dodawaj go do publicznego repozytorium kontroli wersji
Odwoływanie się do KeyStore z aplikacji
Utwórz plik o nazwie <your app dir>/android/key.properties
, który zawiera odwołanie do Twojego 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
.
Dodaj informacje o sklepie kluczy z pliku properties przed blokiem android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Wczytaj plik key.properties
do obiektu keystoreProperties
.
Dodaj ten kod przed blokiem 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
}
}
Skonfiguruj blok signingConfigs
w pliku build.gradle
modułu, podając informacje o konfiguracji podpisywania:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Wersje wersji aplikacji będą teraz podpisywane automatycznie.
Więcej informacji o podpisywaniu aplikacji znajdziesz na stronie Podpisywanie aplikacji na stronie developer.android.com.
Przesyłanie pierwszej wersji
Gdy aplikacja zostanie skonfigurowana do podpisywania, powinnaś/powinieneś mieć możliwość jej skompilowania, uruchamiając:
flutter build appbundle
To polecenie domyślnie generuje kompilację wersji. Dane wyjściowe można znaleźć w folderze <your app dir>/build/app/outputs/bundle/release/
.
Na panelu w Konsoli Google Play kliknij Wersja > Testowanie > Test zamknięty i utwórz nową wersję testów zamkniętych.
W tym ćwiczeniu będziesz korzystać z podpisywania aplikacji przez Google, więc kliknij Dalej w sekcji Podpisywanie aplikacji przez Google Play, aby się do niego zadeklarować.
Następnie prześlij pakiet aplikacji app-release.aab
wygenerowany przez polecenie kompilacji.
Kliknij kolejno Zapisz i Sprawdzanie wersji.
Na koniec kliknij Rozpocznij wdrażanie wersji do testów wewnętrznych, aby aktywować wersję do testów wewnętrznych.
Konfigurowanie użytkowników testowych
Aby umożliwić testowanie zakupów w aplikacji, musisz dodać konta Google testerów w Konsoli Google Play w 2 miejscach:
- na konkretną ścieżkę testów (testy wewnętrzne);
- Jako tester licencji
Najpierw dodaj testera do ścieżki testu wewnętrznego. Wróć do sekcji Publikowanie > Testowanie > Test wewnętrzny i kliknij kartę Testerzy.
Aby utworzyć nową listę e-mailową, kliknij Utwórz listę e-mailową. Nadaj nazwę tej liście i dodaj adresy e-mail kont Google, które mają mieć dostęp 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 mają testować zakupy w aplikacji.
- Ustaw wartość Odpowiedź na prośbę o licencję na
RESPOND_NORMALLY
. - Kliknij Zapisz zmiany.
Konfigurowanie zakupów w aplikacji
Teraz skonfiguruj produkty, które można kupić w aplikacji.
Podobnie jak w App Store, musisz zdefiniować 3 rodzaje zakupów:
dash_consumable_2k
: zakup produktu konsumpcyjnego, który można wielokrotnie kupić, co daje użytkownikowi 2000 Dashes (waluta w aplikacji) za każdy zakup.dash_upgrade_3d
: niewykorzystywane „ulepszenie” do kupienia tylko raz, które daje użytkownikowi wizualnie inny Dash.dash_subscription_doubler
: subskrypcja, która przyznaje użytkownikowi dwukrotnie więcej kresek na kliknięcie przez cały okres subskrypcji.
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 taki sam jak identyfikator, którego zamierzasz użyć.
- Kliknij Zapisz.
- Kliknij Aktywuj.
- Powtórz ten proces w przypadku zakupu „ulepszenia” nieprzemijalnego.
Następnie dodaj subskrypcję:
- Otwórz Konsolę Google Play i wybierz aplikację.
- Kliknij Zarabianie > Produkty > Subskrypcje.
- Kliknij Utwórz subskrypcję
.
- Wpisz wszystkie wymagane informacje dotyczące subskrypcji. Upewnij się, że identyfikator produktu jest taki sam jak identyfikator, którego zamierzasz użyć.
- Kliknij Zapisz.
Twoje zakupy powinny być teraz skonfigurowane w Konsoli Play.
6. Konfigurowanie Firebase
W tym laboratorium kodu użyjesz usługi backendowej do weryfikowania i śledzenia zakupów użytkowników.
Korzystanie z usługi backendowej ma kilka zalet:
- Możesz bezpiecznie weryfikować transakcje.
- Możesz reagować na zdarzenia rozliczeniowe ze sklepów 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 czas w systemie.
Istnieje wiele sposobów konfigurowania usługi backendowej, ale w tym przypadku użyjesz do tego funkcji w chmurze i Firestore, korzystając z usługi Firebase firmy Google.
Tworzenie backendu wykracza poza zakres tego Codelab, dlatego kod startowy zawiera już projekt Firebase, który obsługuje podstawowe zakupy.
Aplikacja startowa zawiera też wtyczki Firebase.
Teraz musisz utworzyć własny projekt Firebase, skonfigurować aplikację i back-end Firebase, a na koniec wdrożyć back-end.
Tworzenie projektu Firebase
Otwórz konsolę Firebase i utwórz nowy projekt Firebase. W tym przykładzie nazwa projektu to Dash Clicker.
W aplikacji na zapleczu łączysz zakupy z konkretnym użytkownikiem, dlatego musisz uwierzytelnić użytkownika. W tym celu użyj modułu uwierzytelniania Firebase z logowaniem w Google.
- W panelu Firebase otwórz sekcję Uwierzytelnianie i w razie potrzeby włącz tę funkcję.
- Otwórz kartę Metoda logowania i włącz dostawcę logowania Google.
Ponieważ będziesz też używać bazy danych Firestore w Firebase, włącz ją też.
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
Zalecanym sposobem instalowania Firebase w aplikacji Flutter jest użycie wiersza poleceń FlutterFire. Postępuj zgodnie z instrukcjami podanymi na stronie konfiguracji.
Podczas uruchamiania 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 wybierz iOS i Android, klikając te 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, kliknij „Tak”.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Konfiguracja Firebase na Androida: dalsze kroki
Na panelu Firebase kliknij Przegląd projektu, wybierz Ustawienia i kartę Ogólne.
Przewiń w dół do sekcji Twoje aplikacje i wybierz aplikację dashclicker (Android).
Aby umożliwić logowanie w Google w trybie debugowania, musisz podać odcisk cyfrowy certyfikatu debugowania w formie hasha SHA-1.
Pobierz hasz certyfikatu podpisywania w celu debugowania
W katalogu głównym projektu aplikacji Flutter zmień katalog na android/
, a następnie wygeneruj raport podpisywania.
cd android ./gradlew :app:signingReport
Zobaczysz dużą listę kluczy podpisywania. Szukasz hasha certyfikatu debugowania, więc znajdź certyfikat z właściwościami Variant
i Config
ustawionymi na debug
. Sklep z kluczami prawdopodobnie znajduje się w folderze domowym w folderze .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 ciąg znaków SHA-1 i wpisz go w ostatnim polu w oknie przesyłania aplikacji.
Konfigurowanie Firebase na iOS: dalsze kroki
Otwórz ios/Runnder.xcworkspace
w aplikacji Xcode
. Możesz też użyć ulubionego środowiska IDE.
W VSCode kliknij prawym przyciskiem folder ios/
, a potem open in xcode
.
W Android Studio kliknij prawym przyciskiem folder ios/
, a potem kliknij kolejno flutter
i opcję open iOS module in Xcode
.
Aby umożliwić logowanie przez Google w iOS, dodaj opcję konfiguracji CFBundleURLTypes
do plików kompilacji plist
. (Aby dowiedzieć się więcej, zapoznaj się z dokumentacją dotyczącą pakietu google_sign_in
). W tym przypadku pliki to ios/Runner/Info-Debug.plist
i ios/Runner/Info-Release.plist
.
Para klucz-wartość została już dodana, ale jej wartości trzeba zastąpić:
- Pobierz wartość
REVERSED_CLIENT_ID
z plikuGoogleService-Info.plist
bez otaczającego go elementu<string>..</string>
. - Zmień wartość w obu plikach
ios/Runner/Info-Debug.plist
iios/Runner/Info-Release.plist
w kluczuCFBundleURLTypes
.
<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. Słuchanie informacji o zakupach
W tej części ćwiczenia przygotujesz aplikację do zakupu produktów. Ten proces obejmuje słuchanie aktualizacji zakupów i błędów po uruchomieniu aplikacji.
Posłuchaj 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 poziomy Provider
dla DashCounter
, DashUpgrades,
i DashPurchases
. DashCounter
śledzi bieżącą liczbę kresek i automatycznie ją zwiększa. DashUpgrades
zarządza uaktualnieniami, które możesz kupić za pomocą Dashes. Ten warsztat dotyczy DashPurchases
.
Domyślnie obiekt dostawcy jest definiowany, gdy po raz pierwszy zostanie wysłane żądanie dotyczące tego obiektu. Ten obiekt nasłuchuje aktualizacji zakupów bezpośrednio po uruchomieniu aplikacji, dlatego wyłącz opóźnione wczytywanie tego obiektu za pomocą 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 zasymulować połączenie. Aby to zrobić, utwórz metodę wystąpienia, 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!;
}
}
Jeśli chcesz, aby test nadal działał, musisz go nieznacznie zaktualizować.
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 aplikacji lib/logic/dash_purchases.dart
otwórz kod DashPurchases ChangeNotifier
. Obecnie do zakupionych urządzeń Dash 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();
}
}
Do _subscription
jest dodawane słowo kluczowe late
, ponieważ _subscription
jest inicjowane w konstruktorze. Ten projekt jest domyślnie skonfigurowany tak, aby nie zezwalać na wartości null (NNBD). Oznacza to, że właściwości, które nie są zadeklarowane jako dopuszczające wartość null, muszą mieć wartość niezerową. Kwalifikator late
umożliwia opóźnienie zdefiniowania tej wartości.
W konstruktorze pobierz strumień purchaseUpdated
i zacznij go słuchać. W metodzie dispose()
anuluj subskrypcję strumienia.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
Aplikacja otrzymuje teraz aktualizacje dotyczące zakupów, więc w następnej sekcji dokonasz zakupu.
Zanim przejdziesz dalej, uruchom testy z opcją „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 tego Codelab zastąpisz obecne fikcyjne produkty prawdziwymi produktami, które można kupić. Produkty te są ładowane ze sklepów, wyświetlane na liście i kupowane po kliknięciu.
Dostosowywanie PurchasableProduct
PurchasableProduct
przedstawia produkt poglądowy. Zaktualizuj go, aby wyświetlał rzeczywiste treści, zastępując klasę PurchasableProduct
w pliku 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ń fikcyjne zakupy i zastąp je pustą listą, List<PurchasableProduct> products = [];
Wczytywanie dostępnych zakupów
Aby umożliwić użytkownikowi dokonanie zakupu, załaduj zakupy ze sklepu. Najpierw sprawdź, czy sklep jest dostępny. Jeśli 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 Firebase zobaczysz storeKeyConsumable
, storeKeySubscription,
i storeKeyUpgrade
. Jeśli oczekiwany zakup jest niedostępny, wydrukuj te informacje na konsoli. Możesz też wysłać te informacje do usługi backendowej.
Metoda await iapConnection.queryProductDetails(ids)
zwraca zarówno identyfikatory, których nie udało się znaleźć, jak i znalezione produkty dostępne do kupienia. Aby zaktualizować interfejs, użyj wartości productDetails
z odpowiedzi i ustaw wartość 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();
}
W konstruktorze wywołaj funkcję loadPurchases()
:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
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 do kupienia
Rozważ plik purchase_page.dart
. Widżet PurchasePage
wyświetla _PurchasesLoading
, _PurchaseList,
lub _PurchasesNotAvailable,
w zależności od StoreState
. Widget pokazuje też wcześniejsze zakupy użytkownika, które są wykorzystywane w następnym kroku.
Widget _PurchaseList
wyświetla listę produktów do kupienia i wysyła prośbę o zakup 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 sklepy na Androida i iOS są prawidłowo skonfigurowane, powinny wyświetlać się dostępne produkty. Pamiętaj, że może minąć trochę czasu, zanim zakupy staną się dostępne po wprowadzeniu na odpowiednie konsole.
Wróć do dash_purchases.dart
i wdróż funkcję umożliwiającą zakup produktu. Wystarczy, że oddzielisz materiały eksploatacyjne od nieeksploatacyjnych. Produkty przejścia na wyższą wersję i subskrypcji nie są produktami jednorazowego użytku.
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 metodę beautifiedDash
getter, aby się do niej odwoływać.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
Metoda _onPurchaseUpdate
odbiera aktualizacje zakupów, aktualizuje stan produktu wyświetlany na stronie zakupu i stosuje zakup do logiki licznika. Po przetworzeniu zakupu ważne jest, aby zadzwonić pod numer completePurchase
, aby sklep wiedział, że zakup został przetworzony prawidłowo.
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 zaczniesz śledzić i weryfikować zakupy, skonfiguruj backend Darta, który to umożliwi.
W tej sekcji użyjesz folderu dart-backend/
jako folderu głównego.
Sprawdź, czy masz zainstalowane te narzędzia:
- Dart
- Firebase CLI
Omówienie projektu podstawowego
Niektóre części tego projektu wykraczają poza zakres tego CodeLab, dlatego zostały one uwzględnione w kodzie startowym. Zanim zaczniesz, warto przejrzeć kod startowy, aby poznać strukturę projektu.
Ten kod backendu może działać lokalnie na Twoim komputerze. Nie musisz go wdrażać, aby go używać. Musisz jednak mieć możliwość połączenia urządzenia deweloperskiego (Androida lub iPhone'a) z maszyną, na której ma działać serwer. Muszą być one w tej samej sieci, a Ty musisz znać adres IP swojego komputera.
Spróbuj uruchomić serwer za pomocą tego polecenia:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
Backend Darta 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.
Część kodu startowego, która jest już zawarta w tym pliku, to IapRepository
w pozycji lib/iap_repository.dart
. W tym samouczku nie uczymy się interakcji z Firestore ani bazami danych w ogóle, dlatego kod startowy zawiera funkcje do tworzenia i aktualizowania zakupów w Firestore oraz wszystkie klasy związane z tymi zakupami.
Konfigurowanie dostępu do Firebase
Aby uzyskać dostęp do Firebase Firestore, potrzebujesz klucza dostępu do konta usługi. Wygeneruj klucz, otwierając ustawienia projektu Firebase i przechodząc do sekcji Konta usługi, a następnie klikając Wygeneruj nowy klucz prywatny.
Skopiuj pobrany plik JSON do folderu assets/
i nazwij go service-account-firebase.json
.
Konfigurowanie dostępu do Google Play
Aby uzyskać dostęp do Sklepu Play w celu weryfikacji zakupów, musisz utworzyć konto usługi z tymi uprawnieniami i pobrać dla niego dane logowania w formacie JSON.
- Otwórz Konsolę Google Play i zacznij od strony Wszystkie aplikacje.
- Kliknij Konfiguracja > Dostęp przez interfejs API.
Jeśli Konsola Google Play poprosi Cię o utworzenie lub połączenie z dotychczasowym projektem, najpierw to zrób, a potem wróć na tę stronę.
- Znajdź sekcję, w której możesz zdefiniować konta usługi, i kliknij Utwórz nowe konto usługi.
- W wyświetlonym oknie kliknij link Google Cloud Platform.
- Wybierz projekt. Jeśli go nie widzisz, sprawdź, czy używasz właściwego konta Google na liście Konto w prawym górnym rogu.
- Po wybraniu projektu na górnym pasku menu kliknij + Utwórz konto usługi.
- Podaj nazwę konta usługi i opcjonalnie opis, aby zapamiętać, do czego służy, a następnie przejdź do następnego kroku.
- Przypisz do konta usługi rolę edytujący.
- Po zakończeniu kreatora wróć w konsoli deweloperów na stronę Dostęp do interfejsu API i kliknij Odśwież konta usługi. Na liście powinno się pojawić nowo utworzone konto.
- Kliknij Przyznaj dostęp dla nowego konta usługi.
- Na następnej stronie przewiń w dół do bloku Dane finansowe. Wybierz Wyświetlanie danych finansowych, zamówień i odpowiedzi z ankiety na temat anulowania oraz Zarządzanie zamówieniami i subskrypcjami.
- Kliknij Zaproś użytkownika.
- Gdy konto jest już skonfigurowane, musisz wygenerować dane logowania. W konsoli Google Cloud znajdź swoje konto usługi na liście kont usługi, kliknij 3 pionowye kropki i wybierz Zarządzaj kluczami.
- Utwórz nowy klucz JSON i pobierz go.
- Zmień nazwę pobranego pliku na
service-account-google-play.json,
i przenieś go do kataloguassets/
.
Musimy też otworzyć 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ć udostępniony klucz tajny:
- Otwórz App Store Connect.
- Otwórz Moje aplikacje i wybierz aplikację.
- W menu na pasku bocznym kliknij Zakupy w aplikacji > Zarządzaj.
- W prawym górnym rogu listy kliknij Sesja tajna aplikacji.
- Wygeneruj nowy obiekt tajny i skopiuj go.
- Otwórz plik
lib/constants.dart,
i zastąp wartośćappStoreSharedSecret
właśnie wygenerowanym wspólnym kluczem tajnym.
Plik konfiguracji stałych
Zanim przejdziesz dalej, sprawdź, czy w pliku lib/constants.dart
skonfigurowane są te stałe:
androidPackageId
: identyfikator pakietu używany na Androidzie, np.com.example.dashclicker
appStoreSharedSecret
: udostępniony klucz tajny umożliwiający dostęp do App Store Connect w celu weryfikacji zakupu.bundleId
: identyfikator pakietu używany na iOS, np.com.example.dashclicker
Na razie możesz zignorować pozostałe stałe.
10. Weryfikacja zakupów
Ogólny proces weryfikacji zakupów jest podobny w przypadku iOS i Androida.
W obu sklepach aplikacja otrzymuje token po dokonaniu zakupu.
Ten token jest wysyłany przez aplikację do usługi backendowej, która z kolei weryfikuje zakup na serwerach odpowiedniego sklepu za pomocą przesłanego tokena.
Usługa backendowa może zapisać zakup i odpowiedzieć aplikacji, czy był on prawidłowy.
Jeśli usługa backendowa będzie weryfikować dane w sklepach, a nie aplikacja działająca na urządzeniu użytkownika, możesz uniemożliwić użytkownikowi uzyskanie dostępu do funkcji premium, np. przez cofnięcie zegara systemowego.
Konfigurowanie strony Flutter
Konfigurowanie uwierzytelniania
Ponieważ zakupy będą wysyłane do usługi backendowej, musisz się upewnić, że użytkownik jest uwierzytelniony podczas zakupu. W tym projekcie startowym większość logiki uwierzytelniania została już dodana. Musisz tylko zadbać o to, aby przycisk logowania PurchasePage
był widoczny, gdy użytkownik nie jest jeszcze zalogowany. Dodaj ten kod na początku metody build w pliku PurchasePage
:
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
Punkt końcowy weryfikacji połączeń z aplikacji
W aplikacji utwórz funkcję _verifyPurchase(PurchaseDetails purchaseDetails)
, która wywołuje punkt końcowy /verifypurchase
w backendzie Dart za pomocą wywołania HTTP POST.
Prześlij wybrany sklep (google_play
dla Sklepu Play lub app_store
dla App Store), serverVerificationData
i productID
. Serwer zwraca kod stanu, który wskazuje, czy zakup został zweryfikowany.
W stałych elementach aplikacji skonfiguruj adres IP serwera jako adres IP komputera lokalnego.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
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 metodę gettera dla obiektu User w klasie FirebaseNotifier, aby móc przekazywać identyfikator użytkownika do funkcji verifyPurchase.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
Dodaj funkcję _verifyPurchase
do klasy DashPurchases
. Funkcja async
zwraca wartość logiczną wskazującą, czy zakup został zatwierdzony.
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 funkcji _handlePurchase
tuż przed zastosowaniem zakupu. Zakup należy zastosować dopiero po jego zweryfikowaniu. W wersji produkcyjnej możesz określić te ustawienia, aby na przykład zastosować subskrypcję próbną, gdy sklep jest tymczasowo niedostępny. W tym przykładzie jednak zachowamy prostotę i zastosujemy zakup dopiero po jego zweryfikowaniu.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(1000);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
W aplikacji wszystko jest już gotowe do zatwierdzenia zakupów.
Konfigurowanie usługi backendu
Następnie skonfiguruj funkcję w chmurze, aby weryfikować zakupy na zapleczu.
Tworzenie modułów obsługi zakupów
Ponieważ proces weryfikacji w obu sklepach jest prawie identyczny, utwórz abstrakcyjną klasę PurchaseHandler
z osobnymi implementacjami dla każdego sklepu.
Najpierw dodaj do folderu lib/
plik purchase_handler.dart
, w którym zdefiniujesz abstrakcyjną klasę PurchaseHandler
z 2 abstrakcyjnymi metodami służącymi do sprawdzania 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 któremu możesz powiązać zakupy z użytkownikiem.productData:
Dane o produkcie. Za chwilę to wyjaśnisz.token:
Token przekazany użytkownikowi przez sklep.
Aby ułatwić korzystanie z tych metod obsługi zakupu, dodaj metodę verifyPurchase()
, która może być używana zarówno w przypadku subskrypcji, jak i w przypadku innych zakupów:
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 możesz wywołać funkcję verifyPurchase
w obu przypadkach, ale nadal masz osobne implementacje.
Klasa ProductData
zawiera podstawowe informacje o różnych produktach do kupienia, w tym identyfikator produktu (czasami nazywany 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 zajęcia, które rozszerzają PurchaseHandler
, który właśnie napisałeś:
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 on true
dla metod obsługi; zajmiesz się nimi później.
Jak widać, konstruktor przyjmuje instancję klasy IapRepository
. Obsługa zakupu używa tej instancji do przechowywania informacji o zakupach w Firestore. Aby komunikować się z Google Play, możesz korzystać z dostępnych 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 zajęcia, które rozszerzają 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;
}
}
Świetnie. Teraz masz 2 elementy obsługi zakupu. Następnie utwórz punkt końcowy interfejsu API weryfikacji zakupu.
Używanie metod obsługi zakupów
Otwórz bin/server.dart
i utwórz punkt końcowy API za pomocą shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
Powyższy kod wykonuje te czynności:
- Zdefiniuj punkt końcowy POST, który będzie wywoływany z utworzonej wcześniej aplikacji.
- Odkoduj ładunek JSON i wyodrębnij te informacje:
userId
: identyfikator zalogowanego użytkownikasource
: użyty sklep:app_store
lubgoogle_play
.productData
: pobrany z utworzonego wcześniejproductDataMap
.token
: zawiera dane weryfikacyjne do wysłania do sklepów.- Wywołaj metodę
verifyPurchase
,GooglePlayPurchaseHandler
lubAppStorePurchaseHandler
, w zależności od źródła. - Jeśli weryfikacja się powiedzie, metoda zwróci klientowi wartość
Response.ok
. - Jeśli weryfikacja się nie powiedzie, metoda zwraca klientowi
Response.internalServerError
.
Po utworzeniu punktu końcowego interfejsu API musisz skonfigurować 2 moduły obsługi zakupu. W tym celu musisz załadować klucze konta usługi uzyskane w poprzednim kroku i skonfigurować dostęp do różnych usług, w tym do interfejsu Android Publisher API i interfejsu Firebase Firestore API. Następnie utwórz 2 obsługi zakupu z różnymi zależnościami:
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,
),
};
}
Weryfikacja zakupów na Androidzie: wdróż obsługę zakupów
Następnie kontynuuj implementowanie modułu obsługi zakupów w Google Play.
Google udostępnia już pakiety Darta do interakcji z interfejsami API, których potrzebujesz do weryfikacji zakupów. Zostały zainicjowane w pliku server.dart
i są teraz używane w klasie GooglePlayPurchaseHandler
.
Zaimplementuj moduł obsługi zakupów innych niż subskrypcja:
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;
}
Obsługę zakupu subskrypcji możesz zaktualizować w podobny sposób:
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;
}
}
Dodaj podaną niżej metodę, aby ułatwić analizowanie identyfikatorów zamówień, a także 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;
}
Twoje zakupy w Google Play powinny zostać zweryfikowane i zapisane w bazie danych.
Następnie przejdź do zakupów w App Store na iOS.
Weryfikacja zakupów w iOS: implementacja modułu obsługi zakupu
Aby weryfikować zakupy w App Store, możesz użyć zewnętrznego pakietu Dart o nazwie app_store_server_sdk
, który ułatwia ten proces.
Zacznij od utworzenia instancji ITunesApi
. Użyj konfiguracji piaskownicy i włącz śledzenie, 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 tych samych punktów końcowych interfejsu API zarówno w przypadku subskrypcji, jak i w przypadku braku subskrypcji. Oznacza to, że możesz używać tej samej logiki w obu obsługiwcach. 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 {
//..
}
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) {
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;
}
}
Twoje zakupy w App Store powinny zostać zweryfikowane i zarchiwizowane w bazie danych.
Uruchom backend
Teraz możesz uruchomić 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
Zalecaną metodą śledzenia zakupów użytkowników jest korzystanie z usługi backendowej. Dzieje się tak, ponieważ backend może reagować na zdarzenia ze sklepu, a więc jest mniej podatny na nieaktualne informacje z powodu buforowania i mniej podatny na manipulację.
Najpierw skonfiguruj przetwarzanie zdarzeń sklepu na zapleczu za pomocą utworzonego przez Ciebie zaplecza Dart.
Przetwarzanie zdarzeń sklepu na zapleczu
Sklepy mogą informować backend o wydarzeniach rozliczeniowych, takich jak odnowienie subskrypcji. Możesz przetwarzać te zdarzenia w backendzie, aby zakupy w Twojej bazie danych były aktualne. W tej sekcji skonfiguruj tę opcję zarówno w Sklepie Google Play, jak i w Apple App Store.
Przetwarzanie zdarzeń rozliczeniowych Google Play
Google Play udostępnia zdarzenia związane z płatnościami za pomocą tematu Cloud Pub/Sub. Są to kolejki wiadomości, na których można publikować wiadomości i z których można je pobierać.
Ponieważ jest to funkcja specyficzna dla Google Play, musisz ją uwzględnić w sekcji GooglePlayPurchaseHandler
.
Najpierw otwórz lib/google_play_purchase_handler.dart
i dodaj import PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Następnie prześlij 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();
});
}
Obiekt Timer
jest skonfigurowany tak, aby co 10 sekund wywoływać metodę _pullMessageFromSubSub
. Możesz dostosować czas trwania do własnych preferencji.
Następnie utwórz _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,
);
}
Dodany przez Ciebie kod co 10 sekund komunikuje się z tematem Pub/Sub z Google Cloud i prosi o nowe wiadomości. Następnie przetwarza każdą wiadomość za pomocą metody _processMessage
.
Ta metoda dekoduje przychodzące wiadomości i uzyskiwanie zaktualizowanych informacji o każdym zakupie (zarówno o subskrypcjach, jak i o innych zakupach), wywołując w razie potrzeby istniejące metody handleSubscription
lub handleNonSubscription
.
Każda wiadomość musi być potwierdzana za pomocą metody _askMessage
.
Następnie dodaj wymagane zależności do pliku server.dart
. Dodaj do konfiguracji danych logowania zakres PubsubApi.cloudPlatform:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
Następnie utwórz instancję PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Na koniec prześlij go do konstruktora GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Konfigurowanie Google Play
Napisany przez Ciebie kod obsługuje zdarzenia płatności z tematu pub/sub, ale nie utworzono tematu pub/sub ani nie publikujesz żadnych zdarzeń płatności. Czas na konfigurację.
Najpierw utwórz temat Pub/Sub:
- 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
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
w elementachconstants.ts
. W tym przypadku nadaj mu nazwęplay_billing
. Jeśli wybierzesz coś innego, pamiętaj, aby zaktualizowaćconstants.ts
. Utwórz temat. - Na liście tematów Pub/Sub kliknij 3 pionowe kropki obok utworzonego właśnie tematu i wybierz Wyświetl uprawnienia.
- Na pasku bocznym po prawej stronie kliknij Dodaj osobę upoważnioną.
- Dodaj konto
google-play-developer-notifications@system.gserviceaccount.com
i nadaj mu rolę Publikujący Pub/Sub. - Zapisz zmiany uprawnień.
- Skopiuj Nazwa tematu utworzonego tematu.
- Ponownie otwórz Konsolę Play i wybierz aplikację z listy Wszystkie aplikacje.
- Przewiń w dół i kliknij Zarabianie > Konfiguracja zarabiania.
- Wypełnij cały temat i zapisz zmiany.
Wszystkie zdarzenia płatności w Google Play będą teraz publikowane w tym temacie.
Przetwarzanie zdarzeń związanych z płatnościami w Sklepie App Store
Następnie wykonaj te same czynności w przypadku zdarzeń rozliczeniowych App Store. Istnieją 2 skuteczne sposoby obsługi aktualizacji zakupów w App Store. Jedną z nich jest wdrożenie webhooka, który udostępniasz firmie Apple i którego używa ona do komunikacji z Twoim serwerem. Drugi sposób, który znajdziesz w tym ćwiczeniu z programowania, polega na połączeniu się z interfejsem App Store Server API i ręcznym uzyskaniu informacji o subskrypcji.
Ten projekt koncentruje się na drugim rozwiązaniu, ponieważ aby zaimplementować webhook, trzeba udostępnić serwer w internecie.
W środowisku produkcyjnym najlepiej jest mieć oba te rozwiązania. Webhook do pobierania zdarzeń z App Store oraz interfejs Server API na wypadek, gdybyś przegapił(-a) zdarzenie lub musiał(-a) ponownie sprawdzić stan subskrypcji.
Najpierw otwórz plik lib/app_store_purchase_handler.dart
i dodaj zależność AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
Zmodyfikuj konstruktor, aby dodać minutnik, który wywoła metodę _pullStatus
. Ten timer będzie wywoływać metodę _pullStatus
co 10 sekund. Czas trwania tego minutnika możesz dostosować 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
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ą interfejsu IapRepository.
- W przypadku każdego zamówienia wysyła żądanie o stan subskrypcji do interfejsu App Store Server API.
- Pobiera ostatnią transakcję dotyczącą zakupu tej subskrypcji.
- Sprawdzanie daty 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
// 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
),
};
Konfiguracja App Store
Następnie skonfiguruj App Store:
- Zaloguj się w App Store Connect i kliknij Użytkownicy i dostęp.
- Kliknij Typ klucza > Zakupy w aplikacji.
- Kliknij ikonę „plusa”, aby dodać nowy plik.
- Nadaj mu nazwę, np. „Klucz Codelab”.
- Pobierz plik p8 zawierający klucz.
- Skopiuj go do folderu zasobów z nazwą
SubscriptionKey.p8
. - Skopiuj identyfikator klucza z nowo utworzonego klucza i ustaw go jako stałą
appStoreKeyId
w plikulib/constants.dart
. - Skopiuj identyfikator Issuer ID znajdujący się na szczycie listy kluczy i przypisz go do stałej wartości
appStoreIssuerId
w plikulib/constants.dart
.
Śledzenie zakupów na urządzeniu
Najbezpieczniejszym sposobem śledzenia zakupów jest śledzenie po stronie serwera, ponieważ trudno jest zabezpieczyć klienta, ale musisz mieć jakiś sposób na przekazanie informacji z powrotem do klienta, aby aplikacja mogła działać na podstawie informacji o stanie subskrypcji. Przechowywanie zakupów w Firestore ułatwia synchronizację danych z klientem i ich automatyczne aktualizowanie.
W aplikacji jest już uwzględnione IAPRepo, czyli repozytorium Firestore zawierające wszystkie dane o zakupach użytkownika w List<PastPurchase> purchases
. Repozytorium zawiera też wartość hasActiveSubscription,
, która jest równa prawdzie, gdy istnieje zakup z wartością productId storeKeySubscription
, którego stan 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((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();
});
}
Cała logika zakupu znajduje się w klasie DashPurchases
i to właśnie tam 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 obiekt typu Listener w konstruktorze i usuń go w metodzie dispose()
. Na początku detektor może być pustą funkcją. Ponieważ IAPRepo
jest ChangeNotifier
, a metodę notifyListeners()
wywołujesz za każdym razem, gdy zakupy w Firestore ulegają zmianie, metoda purchasesUpdate()
jest zawsze wywoływana, gdy zakupione produkty ulegają zmianie.
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
Następnie podaj parametr IAPRepo
konstruktorowi w main.dart.
. Możesz pobrać repozytorium, używając context.read
, ponieważ zostało ono już utworzone w Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
Następnie napisz kod funkcji purchaseUpdate()
. W dash_counter.dart,
metody applyPaidMultiplier
i removePaidMultiplier
mają mnożnik odpowiednio 10 lub 1, więc nie musisz sprawdzać, czy subskrypcja została już zastosowana. 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
na podstawie tego, czy została kupiona wersja wyższa.
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 jest zawsze aktualny w usłudze backendowej i zsynchronizowany z aplikacją. Aplikacja działa odpowiednio i zachowuje funkcje subskrypcji i ulepszenia w grze Dash Clicker.
12. Wszystko gotowe
Gratulacje!!! Ukończyłeś(-aś) kodowanie. Gotowy kod tego ćwiczenia znajdziesz w folderze complete.
Aby dowiedzieć się więcej, zapoznaj się z innymi Codelabs dotyczącymi Fluttera.