Dodawanie zakupów w aplikacji do aplikacji Flutter

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:

  1. możliwość wielokrotnego zakupu 2000 Dashes naraz.
  2. jednorazowy zakup licencji, która umożliwia przekształcenie starego Dash w nowoczesny Dash;
  3. 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.

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

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.

942772eb9a73bfaa.png

W strukturze folderów Xcode na górze znajduje się projekt Runner, a pod nim cele Flutter, RunnerProdukty. 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.

812f919d965c649a.jpeg

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.

5c4733ac560ae8c2.png

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.

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ść).

11db9fca823e7608.png

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.

74c73197472c9aec.png

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.

4a100bbb8cafdbbf.jpeg

Rejestrowanie identyfikatora aplikacji

Utwórz nowy identyfikator na portalu dewelopera Apple.

55d7e592d9a3fc7b.png

Wybieranie identyfikatorów aplikacji

13f125598b72ca77.png

Wybierz aplikację

41ac4c13404e2526.png

Podaj opis i ustaw identyfikator pakietu na taką samą wartość jak w XCode.

9d2c940ad80deeef.png

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.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

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.

3ca2b26d4e391a4c.jpeg

Teraz możesz skonfigurować użytkownika piaskownicy na iPhonie, klikając Ustawienia > Sklep Google Play > Konto piaskownicy.

d99e0b89673867cd.jpeg e1621bcaeb33d3c5.jpeg

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.

d156b2f5bac43ca8.png

Kliknij Zakupy w aplikacji > Zarządzaj.

Tworzenie zakupów w aplikacji za pomocą określonych identyfikatorów:

  1. 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.

ec1701834fd8527.png

  1. 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.

6765d4b711764c30.png

  1. Skonfiguruj dash_subscription_doubler jako automatycznie odnawianą subskrypcję.

Proces subskrypcji wygląda nieco inaczej. Najpierw musisz ustawić nazwę referencyjną i identyfikator produktu:

6d29e08dae26a0c4.png

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.

5bd0da17a85ac076.png

Następnie wpisz okres subskrypcji i lokalizacje. Nazwij tę subskrypcję Jet Engine i nadaj jej opis Doubles your clicks. Kliknij Zapisz.

bd1b1d82eeee4cb3.png

Po kliknięciu przycisku Zapisz dodaj cenę subskrypcji. Wybierz dowolną cenę.

d0bf39680ef0aa2e.png

Na liście zakupów powinny teraz być widoczne 3 zakupy:

99d5c4b446e8fecf.png

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:

  1. Otwórz Konsolę Play.
  2. Kliknij Wszystkie aplikacje > Utwórz aplikację.
  3. 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ć.
  4. Potwierdź, że Twoja aplikacja jest grą. Możesz to później zmienić.
  5. Określ, czy aplikacja ma być bezpłatna czy płatna.
  6. Dodaj adres e-mail, pod którym użytkownicy Sklepu Play będą mogli kontaktować się z Tobą w sprawie tej aplikacji.
  7. Wypełnij deklarację „Wytyczne dotyczące treści” i „Przepisy eksportowe USA”.
  8. 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. 13845badcf9bc1db.png

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ć.

ba98446d9c5c40e0.png

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:

  1. na konkretną ścieżkę testów (testy wewnętrzne);
  2. Jako tester licencji

Najpierw dodaj testera do ścieżki testu wewnętrznego. Wróć do sekcji Publikowanie > Testowanie > Test wewnętrzny i kliknij kartę Testerzy.

a0d0394e85128f84.png

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:

  1. Wróć do widoku Wszystkie aplikacje w Konsoli Google Play.
  2. Kliknij Ustawienia > Testowanie licencji.
  3. Dodaj te same adresy e-mail testerów, którzy mają testować zakupy w aplikacji.
  4. Ustaw wartość Odpowiedź na prośbę o licencję na RESPOND_NORMALLY.
  5. Kliknij Zapisz zmiany.

a1a0f9d3e55ea8da.png

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.

  1. Otwórz Konsolę Google Play i wybierz aplikację.
  2. Kliknij Zarabianie > Produkty > Produkty w aplikacji.
  3. Kliknij Utwórz produktc8d66e32f57dee21.png.
  4. Wpisz wszystkie wymagane informacje o produkcie. Upewnij się, że identyfikator produktu jest taki sam jak identyfikator, którego zamierzasz użyć.
  5. Kliknij Zapisz.
  6. Kliknij Aktywuj.
  7. Powtórz ten proces w przypadku zakupu „ulepszenia” nieprzemijalnego.

Następnie dodaj subskrypcję:

  1. Otwórz Konsolę Google Play i wybierz aplikację.
  2. Kliknij Zarabianie > Produkty > Subskrypcje.
  3. Kliknij Utwórz subskrypcję32a6a9eefdb71dd0.png.
  4. Wpisz wszystkie wymagane informacje dotyczące subskrypcji. Upewnij się, że identyfikator produktu jest taki sam jak identyfikator, którego zamierzasz użyć.
  5. 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.

  1. W panelu Firebase otwórz sekcję Uwierzytelnianie i w razie potrzeby włącz tę funkcję.
  2. Otwórz kartę Metoda logowania i włącz dostawcę logowania Google.

7babb48832fbef29.png

Ponieważ będziesz też używać bazy danych Firestore w Firebase, włącz ją też.

e20553e0de5ac331.png

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 iOSAndroid, 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).

b22d46a759c0c834.png

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ć:

  1. Pobierz wartość REVERSED_CLIENT_ID z pliku GoogleService-Info.plist bez otaczającego go elementu <string>..</string>.
  2. Zmień wartość w obu plikach ios/Runner/Info-Debug.plist i ios/Runner/Info-Release.plist w kluczu CFBundleURLTypes.
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

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

main.dart, znajdź widżet MyHomePage, który ma ScaffoldBottomNavigationBar zawierającym 2 strony. Ta strona tworzy też 3 poziomy Provider dla DashCounter, DashUpgrades,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;
}

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,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 storeStateStoreState.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.

ca1a9f97c21e552d.png

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:

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 shelfshelf_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.

27590fc77ae94ad4.png

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.

  1. Otwórz Konsolę Google Play i zacznij od strony Wszystkie aplikacje.
  2. Kliknij Konfiguracja > Dostęp przez interfejs API. 317fdfb54921f50e.png 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ę.
  3. Znajdź sekcję, w której możesz zdefiniować konta usługi, i kliknij Utwórz nowe konto usługi.1e70d3f8d794bebb.png
  4. W wyświetlonym oknie kliknij link Google Cloud Platform. 7c9536336dd9e9b4.png
  5. 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. 3fb3a25bad803063.png
  6. Po wybraniu projektu na górnym pasku menu kliknij + Utwórz konto usługi. 62fe4c3f8644acd8.png
  7. Podaj nazwę konta usługi i opcjonalnie opis, aby zapamiętać, do czego służy, a następnie przejdź do następnego kroku. 8a92d5d6a3dff48c.png
  8. Przypisz do konta usługi rolę edytujący. 6052b7753667ed1a.png
  9. 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. 5895a7db8b4c7659.png
  10. Kliknij Przyznaj dostęp dla nowego konta usługi.
  11. 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. 75b22d0201cf67e.png
  12. Kliknij Zaproś użytkownika. 70ea0b1288c62a59.png
  13. 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. 853ee186b0e9954e.png
  14. Utwórz nowy klucz JSON i pobierz go. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Zmień nazwę pobranego pliku na service-account-google-play.json, i przenieś go do katalogu assets/.

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:

  1. Otwórz App Store Connect.
  2. Otwórz Moje aplikacje i wybierz aplikację.
  3. W menu na pasku bocznym kliknij Zakupy w aplikacji > Zarządzaj.
  4. W prawym górnym rogu listy kliknij Sesja tajna aplikacji.
  5. Wygeneruj nowy obiekt tajny i skopiuj go.
  6. Otwórz plik lib/constants.dart, i zastąp wartość appStoreSharedSecret właśnie wygenerowanym wspólnym kluczem tajnym.

d8b8042470aaeff.png

b72f4565750e2f40.png

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), serverVerificationDataproductID. 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 DashPurchasesmain.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.

be50c207c5a2a519.png

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:

  1. Zdefiniuj punkt końcowy POST, który będzie wywoływany z utworzonej wcześniej aplikacji.
  2. Odkoduj ładunek JSON i wyodrębnij te informacje:
  3. userId: identyfikator zalogowanego użytkownika
  4. source: użyty sklep: app_store lub google_play.
  5. productData: pobrany z utworzonego wcześniej productDataMap.
  6. token: zawiera dane weryfikacyjne do wysłania do sklepów.
  7. Wywołaj metodę verifyPurchase, GooglePlayPurchaseHandler lub AppStorePurchaseHandler, w zależności od źródła.
  8. Jeśli weryfikacja się powiedzie, metoda zwróci klientowi wartość Response.ok.
  9. 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:

  1. Otwórz stronę Cloud Pub/Sub w konsoli Google Cloud.
  2. Upewnij się, że jesteś w projekcie Firebase, i kliknij + Utwórz temat. d5ebf6897a0a8bf5.png
  3. Nadaj nowemu tematowi nazwę identyczną z wartością ustawioną dla GOOGLE_PLAY_PUBSUB_BILLING_TOPIC w elementach constants.ts. W tym przypadku nadaj mu nazwę play_billing. Jeśli wybierzesz coś innego, pamiętaj, aby zaktualizować constants.ts. Utwórz temat. 20d690fc543c4212.png
  4. Na liście tematów Pub/Sub kliknij 3 pionowe kropki obok utworzonego właśnie tematu i wybierz Wyświetl uprawnienia. ea03308190609fb.png
  5. Na pasku bocznym po prawej stronie kliknij Dodaj osobę upoważnioną.
  6. Dodaj konto google-play-developer-notifications@system.gserviceaccount.com i nadaj mu rolę Publikujący Pub/Sub. 55631ec0549215bc.png
  7. Zapisz zmiany uprawnień.
  8. Skopiuj Nazwa tematu utworzonego tematu.
  9. Ponownie otwórz Konsolę Play i wybierz aplikację z listy Wszystkie aplikacje.
  10. Przewiń w dół i kliknij Zarabianie > Konfiguracja zarabiania.
  11. Wypełnij cały temat i zapisz zmiany. 7e5e875dc6ce5d54.png

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:

  1. Pobiera listę aktywnych subskrypcji z Firestore za pomocą interfejsu IapRepository.
  2. W przypadku każdego zamówienia wysyła żądanie o stan subskrypcji do interfejsu App Store Server API.
  3. Pobiera ostatnią transakcję dotyczącą zakupu tej subskrypcji.
  4. Sprawdzanie daty ważności.
  5. 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:

  1. Zaloguj się w App Store Connect i kliknij Użytkownicy i dostęp.
  2. Kliknij Typ klucza > Zakupy w aplikacji.
  3. Kliknij ikonę „plusa”, aby dodać nowy plik.
  4. Nadaj mu nazwę, np. „Klucz Codelab”.
  5. Pobierz plik p8 zawierający klucz.
  6. Skopiuj go do folderu zasobów z nazwą SubscriptionKey.p8.
  7. Skopiuj identyfikator klucza z nowo utworzonego klucza i ustaw go jako stałą appStoreKeyId w pliku lib/constants.dart.
  8. Skopiuj identyfikator Issuer ID znajdujący się na szczycie listy kluczy i przypisz go do stałej wartości appStoreIssuerId w pliku lib/constants.dart.

9540ea9ada3da151.png

Ś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 applyPaidMultiplierremovePaidMultiplier 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 android_studio_folder.pngcomplete.

Aby dowiedzieć się więcej, zapoznaj się z innymi Codelabs dotyczącymi Fluttera.