Dodawanie zakupów w aplikacji do aplikacji Flutter

1. Wprowadzenie

Ostatnia aktualizacja: 11.07.2023 r.

Dodanie zakupów w aplikacji do aplikacji Flutter wymaga poprawnego skonfigurowania Sklepu App i Sklepu Play, zweryfikowania zakupu oraz przyznania niezbędnych uprawnień, np. bonusów z subskrypcji.

W ramach tego ćwiczenia w Codelabs dodasz 3 typy zakupów w aplikacji do aplikacji (udostępnione przez Ciebie) i potwierdzisz te zakupy za pomocą backendu Dart z Firebase. Udostępniona aplikacja, Dash Clicker, zawiera grę, w której walutą jest maskotka Dash. Dodasz te opcje zakupu:

  1. Opcja wielokrotnego zakupu za 2000 kresek.
  2. Jednorazowy zakup ulepszenia, który pozwoli Ci zamienić starą wersję Dash w nowoczesną wersję.
  3. Subskrypcja, która podwaja liczbę kliknięć generowanych automatycznie.

Pierwsza opcja zakupu zapewnia użytkownikowi bezpośrednie korzyści w postaci 2000 kresek. Są one dostępne bezpośrednio dla użytkowników i można je kupić wiele razy. To właśnie produkty konsumpcyjne, ponieważ są spożywane bezpośrednio i można je wielokrotnie spożywać.

Druga opcja sprawi, że Dash będzie piękniejszy. Trzeba ją kupić tylko raz. Jest ona dostępna bezterminowo. Taki zakup określa się jako niemożliwy do zużycia, ponieważ aplikacja nie może go wykorzystać, ale jest ważny bezterminowo.

Trzecią i ostatnią opcją zakupu jest subskrypcja. Gdy subskrypcja jest aktywna, użytkownik szybciej uzyskuje dostęp do Dashe, ale gdy przestanie płacić za subskrypcję, korzyści też utracą.

Usługa backendu (dostępna również dla Ciebie) działa jako aplikacja Dart, weryfikuje, czy zakupy są dokonywane, i przechowuje je w Firestore. Firestore ułatwia proces, ale w aplikacji produkcyjnej możesz korzystać z dowolnego typu usługi backendu.

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

Co utworzysz

  • Przedłużysz aplikację o obsługę zakupów produktów konsumpcyjnych i subskrypcji.
  • Rozszerzysz też aplikację backendu DART do weryfikacji i przechowywania zakupionych elementów.

Czego się nauczysz

  • Jak skonfigurować produkty do kupienia w App Store i Sklepie Play.
  • Jak komunikować się ze sklepami w celu weryfikowania zakupów i przechowywania ich w Firestore.
  • Jak zarządzać zakupami w aplikacji

Czego potrzebujesz

  • Android Studio 4.1 lub nowszy
  • Xcode 12 lub nowsza wersja (na potrzeby programowania na iOS)
  • Pakiet SDK Flutter

2. Konfigurowanie środowiska programistycznego

Aby rozpocząć to ćwiczenie w Codelabs, pobierz kod i zmień identyfikator pakietu w przypadku 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 GitHub, użyj tego polecenia:

gh repo clone flutter/codelabs flutter-codelabs

Przykładowy kod zostanie skopiowany do katalogu flutter-codelabs, który zawiera kod kolekcji ćwiczeń z programowania. Kod do tego ćwiczenia w Codelabs jest dostępny w języku flutter-codelabs/in_app_purchases.

Struktura katalogów w flutter-codelabs/in_app_purchases zawiera serię zrzutów ekranu, w których powinno znajdować się miejsce na końcu każdego nazwanego kroku. Kod startowy znajduje się w kroku 0, więc zlokalizowanie pasujących plików jest dziecinnie proste:

cd flutter-codelabs/in_app_purchases/step_00

Jeśli chcesz przejść do następnego kroku lub zobaczyć, jak powinien wyglądać dany krok, zajrzyj do katalogu o nazwie odpowiadającej interesującemu Cię kroku. Kod z ostatniego kroku znajduje się w folderze complete.

Konfigurowanie projektu startowego

Otwórz projekt początkowy z step_00 w swoim ulubionym IDE. Do tworzenia zrzutów ekranu użyliśmy Androida Studio, ale świetnym rozwiązaniem jest też Visual Studio Code. W obu edytorze sprawdź, czy masz zainstalowane najnowsze wtyczki Dart i Flutter.

Aplikacje, które planujesz skonfigurować, będą musiały komunikować się z App Store i Sklepem Play, aby dowiedzieć się, które produkty są dostępne i w jakiej cenie. Każda aplikacja ma niepowtarzalny identyfikator. W sklepie App Store jest to identyfikator pakietu, a w Sklepie Play – identyfikator aplikacji. Identyfikatory te są zwykle tworzone w odwrotnym zapisie nazwy domeny. Na przykład do tworzenia aplikacji do zakupu w aplikacji dla flutter.dev użyjemy dev.flutter.inapppurchase. Pomyśl o identyfikatorze swojej aplikacji i ustaw go teraz w ustawieniach projektu.

Najpierw skonfiguruj identyfikator pakietu dla iOS.

Po otwarciu projektu w Android Studio kliknij prawym przyciskiem myszy folder iOS, kliknij Flutter i otwórz moduł w aplikacji Xcode.

942772eb9a73bfaa.png

W strukturze folderów Xcode u góry znajduje się Projekt Runner, a cele Flutter, Runner i Products poniżej projektu Runner. Kliknij dwukrotnie Runner, aby edytować ustawienia projektu, a następnie kliknij Podpisanie Możliwości. Aby określić zespół, wpisz wybrany przed chwilą identyfikator pakietu w polu Zespół.

812f919d965c649a.jpeg

Możesz teraz zamknąć Xcode i wrócić do Android Studio, by dokończyć konfigurację dla Androida. Aby to zrobić, otwórz plik build.gradle w katalogu android/app, i zmień applicationId (w wierszu 37 na zrzucie ekranu poniżej) na identyfikator aplikacji, taki jak identyfikator pakietu na iOS. Pamiętaj, że identyfikatory dla sklepów z iOS i Androidem nie muszą być identyczne, ale pozostawienie ich identycznych jest mniej podatne na błędy i dlatego w tym ćwiczeniu w programowaniu używamy również identycznych identyfikatorów.

5c4733ac560ae8c2.png

3. Zainstaluj wtyczkę

W tej części ćwiczenia z programowania zainstalujesz wtyczkę in_app_purchase.

Dodaj zależność w specyfikacji pubspec

Dodaj in_app_purchase do specyfikacji wydawcy, dodając in_app_purchase do zależności w specyfikacji publik:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

dependencies:
  ..
  cloud_firestore: ^4.0.3
  firebase_auth: ^4.2.2
  firebase_core: ^2.5.0
  google_sign_in: ^6.0.1
  http: ^0.13.4
  in_app_purchase: ^3.0.1
  intl: ^0.18.0
  provider: ^6.0.2
  ..

Kliknij pub get, aby pobrać pakiet, lub uruchom polecenie flutter pub get w wierszu poleceń.

4. Skonfiguruj App Store

Aby skonfigurować zakupy w aplikacji i przetestować je w systemie iOS, musisz utworzyć nową aplikację w App Store, a w niej utworzyć produkty, które można kupić. Nie musisz niczego publikować ani przesyłać aplikacji do sprawdzenia Apple. Aby to zrobić, musisz mieć konto dewelopera. Jeśli go nie masz, zarejestruj się w programie Apple dla deweloperów.

Aby korzystać z zakupów w aplikacji, musisz też mieć aktywną umowę na płatne aplikacje w App Store Connect. Wejdź na https://appstoreconnect.apple.com/ i kliknij Umowy, podatki i bankowość.

6e373780e5e24a6f.png

Tutaj znajdziesz umowy dotyczące bezpłatnych i płatnych aplikacji. Stan bezpłatnych aplikacji powinien być aktywny, a stan płatnych aplikacji powinien być nowy. Zapoznaj się z warunkami, zaakceptuj je i wpisz wszystkie wymagane informacje.

74c73197472c9aec.png

Po ustawieniu wszystkich opcji płatne aplikacje będą miały stan aktywny. To bardzo ważne, ponieważ bez aktywnej umowy nie możesz wypróbować zakupów w aplikacji.

4a100bbb8cafdbbf.jpeg

Zarejestruj identyfikator aplikacji

Utwórz nowy identyfikator w portalu Apple dla deweloperów.

55d7e592d9a3fc7b.png

Wybierz identyfikatory aplikacji

13f125598b72ca77.png

Wybierz aplikację

41ac4c13404e2526.png

Podaj opis i ustaw identyfikator pakietu tak, aby identyfikator pakietu miał taką samą wartość jak wcześniej ustawiona w XCode.

9d2c940ad80deeef.png

Więcej wskazówek o tworzeniu nowego identyfikatora aplikacji znajdziesz w Centrum pomocy konta dewelopera .

Tworzenie nowej aplikacji

Utwórz nową aplikację w App Store Connect, używając unikalnego identyfikatora pakietu.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Więcej wskazówek na temat tworzenia nowych aplikacji i zarządzania umowami znajdziesz w pomocy do App Store Connect.

Aby testować zakupy w aplikacji, potrzebujesz testowego użytkownika piaskownicy. 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 Piaskownica, aby utworzyć nowe konto w trybie piaskownicy lub zarządzać dotychczasowymi identyfikatorami Apple ID piaskownicy.

3ca2b26d4e391a4c.jpeg

Teraz możesz skonfigurować na iPhonie użytkownika sandbox. W tym celu wybierz kolejno Settings > (Ustawienia >) Sklep z aplikacjami > Konto w trybie piaskownicy.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

Konfigurowanie zakupów w aplikacji

Następnie skonfigurujesz te 3 elementy, które można kupić:

  • dash_consumable_2k: produkt konsumpcyjny, który można kupić wielokrotnie, a użytkownik otrzymuje 2000 kresek (waluty w aplikacji) na każdy zakup.
  • dash_upgrade_3d: nieużywana „aktualizacja” zakup, który można kupić tylko raz.
  • dash_subscription_doubler: subskrypcja, w ramach której użytkownik przyznaje użytkownikowi 2 razy więcej kresek na kliknięcie.

d156b2f5bac43ca8.png

Wybierz Zakupy w aplikacji >”. Zarządzaj.

Twórz zakupy w aplikacji o określonych identyfikatorach:

  1. Skonfiguruj dash_consumable_2k jako Towar konsumpcyjny.

Użyj dash_consumable_2k jako identyfikatora produktu. Nazwa referencyjna jest używana tylko w przypadku połączenia ze sklepem z aplikacjami. Wystarczy ustawić ją na dash consumable 2k i dodać lokalizacje zakupu. Wywołaj zakup Spring is in the air z opisem 2000 dashes fly out.

ec1701834fd8527.png

  1. Skonfiguruj urządzenie dash_upgrade_3d jako nieużywane.

Użyj dash_upgrade_3d jako identyfikatora produktu. Jako nazwę referencyjną ustaw dash upgrade 3d i dodaj lokalizacje zakupu. Wywołaj zakup 3D Dash z opisem 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 ustaw nazwę referencyjną i identyfikator produktu:

6d29e08dae26a0c4.png

Musisz teraz utworzyć grupę subskrypcji. Gdy w grupie jest wiele subskrypcji, użytkownik może w tym samym czasie subskrybować tylko jeden z nich, ale może łatwo przejść na wyższą lub niższą wersję. Po prostu zadzwoń do tej grupy: subscriptions.

5bd0da17a85ac076.png

Następnie wpisz czas trwania subskrypcji i lokalizacje. Nazwij ją Jet Engine i nadaj jej opisowi Doubles your clicks. Kliknij Zapisz.

bd1b1d82eeee4cb3.png

Po kliknięciu przycisku Zapisz dodaj cenę subskrypcji. Możesz wybrać dowolną cenę.

d0bf39680ef0aa2e.png

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

99d5c4b446e8fecf.png

5. Konfigurowanie Sklepu Play

Podobnie jak w przypadku App Store, musisz mieć też konto dewelopera w Sklepie Play. Jeśli go nie masz, zarejestruj się.

Tworzenie nowej aplikacji

Utwórz nową aplikację w Konsoli Google Play:

  1. Otwórz Konsolę Play.
  2. Wybierz Wszystkie aplikacje > Utwórz aplikację.
  3. Wybierz język domyślny i nazwij aplikację. Wpisz nazwę aplikacji, która ma wyświetlać się w Google Play. Możesz ją później zmienić.
  4. Określ, że 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 mogą kontaktować się z Tobą w sprawie tej aplikacji.
  7. Wypełnij wytyczne dotyczące treści i deklaracje dotyczące przepisów eksportowych Stanów Zjednoczonych.
  8. Wybierz Utwórz aplikację.

Po utworzeniu aplikacji otwórz panel i wykonaj wszystkie zadania w sekcji Konfigurowanie aplikacji. W tym miejscu znajdziesz informacje o aplikacji, takie jak oceny treści czy zrzuty ekranu. 13845badcf9bc1db.png

Podpisywanie zgłoszenia

Aby testować zakupy w aplikacji, musisz przesłać do Google Play co najmniej 1 kompilację.

W tym celu kompilacja wersji musi być podpisana czymś innym niż klucze debugowania.

Utwórz magazyn kluczy

Jeśli masz już magazyn kluczy, przejdź do następnego kroku. Jeśli tak nie jest, utwórz je, uruchamiając w wierszu poleceń podane niżej polecenie.

W systemie Mac lub Linux 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 głównym. Jeśli chcesz zapisać plik w innym miejscu, zmień argument przekazywany do parametru -keystore. Zachowaj

keystore

file private; nie sprawdzaj ich w kontroli źródeł publicznych.

Dodaj odwołanie do magazynu kluczy 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 logowania Gradle

Skonfiguruj podpisywanie aplikacji, edytując plik <your app dir>/android/app/build.gradle.

Dodaj informacje o magazynie kluczy z pliku właściwości 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
       }
   }

Kompilacje wersji Twojej aplikacji będą teraz podpisywane automatycznie.

Więcej informacji o podpisywaniu aplikacji znajdziesz w artykule Podpisywanie aplikacji na stronie developer.android.com.

Prześlij swoją pierwszą kompilację

Gdy skonfigurujesz aplikację do podpisywania, możesz ją skompilować, uruchamiając polecenie:

flutter build appbundle

To polecenie domyślnie generuje kompilację wersji, a dane wyjściowe znajdziesz w <your app dir>/build/app/outputs/bundle/release/

W panelu w Konsoli Google Play kliknij Wersja > Testowanie > test zamknięty i utwórz nową wersję do testów zamkniętych.

W przypadku tych ćwiczeń w programie musisz pozostać przy podpisywaniu aplikacji przez Google, więc kliknij Dalej w sekcji Podpisywanie aplikacji przez Google Play, aby wyrazić zgodę.

ba98446d9c5c40e0.png

Następnie prześlij pakiet aplikacji app-release.aab wygenerowany za pomocą polecenia Build.

Kliknij kolejno Zapisz i Sprawdzanie wersji.

Na koniec kliknij Rozpocznij wdrażanie do testów wewnętrznych, aby aktywować wersję do testów wewnętrznych.

Konfigurowanie użytkowników testowych

Aby móc testować zakupy w aplikacji, musisz dodać w Konsoli Google Play konta Google testerów w 2 lokalizacjach:

  1. Na konkretną ścieżkę testu (test wewnętrzny)
  2. Jako tester licencji

Zacznij od dodania testera do ścieżki testu wewnętrznego. Wróć do Wersja > Testowanie > testów wewnętrznych i kliknij kartę Testerzy.

a0d0394e85128f84.png

Utwórz nową listę e-mailową, klikając Utwórz listę e-mailową. Nazwij listę i dodaj adresy e-mail kont Google, które mają mieć dostęp do testowania zakupów w aplikacji.

Następnie zaznacz pole wyboru listy i kliknij Zapisz zmiany.

Następnie dodaj testerów licencji:

  1. Wróć do widoku Wszystkie aplikacje w Konsoli Google Play.
  2. Wybierz kolejno Ustawienia > Testowanie licencji.
  3. Dodaj te same adresy e-mail testerów, którzy mają mieć możliwość testowania zakupów w aplikacji.
  4. Ustaw Odpowiedź dotyczącą licencji na RESPOND_NORMALLY.
  5. Kliknij Zapisz zmiany.

a1a0f9d3e55ea8da.png

Konfigurowanie zakupów w aplikacji

Teraz skonfigurujesz elementy, które można kupić w aplikacji.

Podobnie jak w przypadku sklepu z aplikacjami, musisz zdefiniować trzy różne zakupy:

  • dash_consumable_2k: produkt konsumpcyjny, który można kupić wielokrotnie, a użytkownik otrzymuje 2000 kresek (waluty w aplikacji) na każdy zakup.
  • dash_upgrade_3d: nieużywana „aktualizacja” zakup, który można kupić tylko raz.
  • dash_subscription_doubler: subskrypcja, w ramach której użytkownik przyznaje użytkownikowi 2 razy więcej kresek na kliknięcie.

Najpierw dodaj produkty zużywalne i nieużywane.

  1. Otwórz Konsolę Google Play i wybierz aplikację.
  2. Przejdź do sekcji Zarabianie >”. Produkty > Produkty w aplikacji.
  3. Kliknij Utwórz produktc8d66e32f57dee21.png.
  4. Wpisz wszystkie wymagane informacje o produkcie. Upewnij się, że identyfikator produktu jest dokładnie taki sam jak identyfikator, którego zamierzasz użyć.
  5. Kliknij Zapisz.
  6. Kliknij Aktywuj.
  7. Powtórz ten proces w przypadku „aktualizacji”, która nie jest zużywana. zakup.

Następnie dodaj subskrypcję:

  1. Otwórz Konsolę Google Play i wybierz aplikację.
  2. Przejdź do sekcji Zarabianie >”. Produkty > Subskrypcje.
  3. Kliknij Utwórz subskrypcję32a6a9eefdb71dd0.png.
  4. Wpisz wszystkie wymagane informacje o subskrypcji. Upewnij się, że identyfikator produktu jest dokładnie taki sam jak identyfikator, którego chcesz użyć.
  5. Kliknij Zapisz.

Twoje zakupy powinny być teraz skonfigurowane w Konsoli Play.

6. Skonfiguruj Firebase

W ramach tego ćwiczenia w Codelabs wykorzystasz usługę backendu do weryfikowania i śledzenia zakupów.

Korzystanie z usługi backendu ma kilka zalet:

  • Transakcje możesz weryfikować w bezpieczny sposób.
  • Możesz reagować na zdarzenia związane z płatnościami ze sklepów z aplikacjami.
  • Zakupy możesz śledzić w bazie danych.
  • Użytkownicy nie będą mogli nakłonić Twojej aplikacji do udostępnienia funkcji premium, przewijając do tyłu zegar systemowy.

Usługę backendu można skonfigurować na wiele sposobów, ale możesz to zrobić za pomocą funkcji w Cloud Functions i Firestore, korzystając z opracowanej przez Google Firebase.

Uznaje się, że pisanie backendu nie obejmuje tego ćwiczenia z programowania, dlatego na początek kod startowy zawiera już projekt Firebase, który obsługuje podstawowe zakupy.

Wtyczki Firebase są też dołączone do aplikacji startowej.

Musisz jeszcze tylko utworzyć własny projekt Firebase, skonfigurować aplikację i backend dla Firebase, a na koniec wdrożyć backend.

Tworzenie projektu Firebase

Otwórz konsolę Firebase i utwórz nowy projekt Firebase. W tym przykładzie użyj nazwy projektu Dash Clicker.

W aplikacji backendu musisz powiązać zakupy z konkretnym użytkownikiem, dlatego potrzebujesz uwierzytelnienia. Wykorzystaj do tego moduł uwierzytelniania Firebase z logowaniem przez Google.

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

7babb48832fbef29.png

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

e20553e0de5ac331.png

Ustaw reguły Cloud Firestore w następujący 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 na potrzeby Flutter

Zalecanym sposobem zainstalowania Firebase w aplikacji Flutter jest użycie interfejsu wiersza poleceń FlutterFire. Postępuj zgodnie z instrukcjami na stronie konfiguracji.

Po uruchomieniu Flutterfire Configure wybierz projekt utworzony w poprzednim kroku.

$ flutterfire configure

i Found 5 Firebase projects.                                                                                                  
? Select a Firebase project to configure your Flutter application with ›                                                      
❯ in-app-purchases-1234 (in-app-purchases-1234)                                                                         
  other-flutter-codelab-1 (other-flutter-codelab-1)                                                                           
  other-flutter-codelab-2 (other-flutter-codelab-2)                                                                      
  other-flutter-codelab-3 (other-flutter-codelab-3)                                                                           
  other-flutter-codelab-4 (other-flutter-codelab-4)                                                                                                                                                               
  <create a new project>  

Następnie włącz iOS i Androida, wybierając te platformy.

? Which platforms should your configuration support (use arrow keys & space to select)? ›                                     
✔ android                                                                                                                     
✔ ios                                                                                                                         
  macos                                                                                                                       
  web                                                                                                                          

Gdy pojawi się prośba o zastąpienie pliku firebase_options.dart, wybierz Tak.

? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes                                                                                                                         

Konfigurowanie Firebase na Androida: dalsze kroki

W panelu Firebase kliknij Przegląd projektu, kliknij Ustawienia i wybierz kartę Ogólne.

Przewiń w dół do sekcji Twoje aplikacje i wybierz aplikację dashclicker (android).

b22d46a759c0c834.png

Aby zezwolić na logowanie się przez Google w trybie debugowania, musisz podać odcisk cyfrowy SHA-1 swojego certyfikatu debugowania.

Pobierz hasz certyfikatu podpisywania na potrzeby debugowania

W katalogu głównym projektu aplikacji Flutter zmień katalog na folder android/, a następnie wygeneruj raport podpisywania.

cd android
./gradlew :app:signingReport

Pojawi się obszerna lista kluczy podpisywania. Szukasz skrótu certyfikatu debugowania, więc szukaj certyfikatu z właściwościami Variant i Config ustawionymi na wartość debug. Magazyn kluczy prawdopodobnie znajduje się w folderze głównym pod adresem .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 hasz SHA-1 i wypełnij ostatnie pole w oknie modalnym przesyłania aplikacji.

Konfigurowanie Firebase na iOS: dalsze kroki

Otwórz: ios/Runnder.xcworkspace za pomocą aplikacji Xcode. Możesz też użyć wybranego środowiska IDE.

W VSCode kliknij prawym przyciskiem myszy folder ios/, a potem kliknij open in xcode.

W Android Studio kliknij prawym przyciskiem myszy folder ios/, a następnie kliknij flutter i opcję open iOS module in Xcode.

Aby umożliwić logowanie przez Google na urządzeniach z iOS, dodaj opcję konfiguracji CFBundleURLTypes do plików kompilacji plist. (Więcej informacji znajdziesz w dokumentacji 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 należy zastąpić ich wartości:

  1. Pobierz wartość pola REVERSED_CLIENT_ID z pliku GoogleService-Info.plist bez otaczającego go elementu <string>..</string>.
  2. Zastąp wartość w 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>

To już koniec konfiguracji Firebase.

7. Odsłuchaj najnowsze informacje o zakupach

W tej części ćwiczenia w Codelabs przygotujesz aplikację do kupowania produktów. Ten proces obejmuje nasłuchiwanie aktualizacji zakupów i błędów po uruchomieniu aplikacji.

Słuchaj aktualności dotyczących zakupów

W narzędziu main.dart, znajdź widżet MyHomePage, który zawiera element Scaffold z wartością BottomNavigationBar obejmującą 2 strony. Na tej stronie są też tworzone 3 elementy Provider dla DashCounter, DashUpgrades, i DashPurchases. DashCounter śledzi bieżącą liczbę kresek i automatycznie je zwiększa. Uaktualnieniami, które możesz kupić za pomocą Dashes, zarządza DashUpgrades. To ćwiczenie w programie koncentruje się na tych zagadnieniach: DashPurchases.

Obiekt dostawcy jest domyślnie definiowany przy pierwszym żądaniu tego obiektu. Ten obiekt nasłuchuje aktualizacji zakupów bezpośrednio po uruchomieniu aplikacji, więc wyłącz leniwe ładowanie w tym obiekcie za pomocą polecenia lazy: false:

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create: (context) => DashPurchases(
    context.read<DashCounter>(),
  ),
  lazy: false,
),

Potrzebujesz też instancji InAppPurchaseConnection. Aby jednak utrzymać możliwość testowania aplikacji, musisz mieć jakiś sposób na imitowanie połączenia. W tym celu utwórz metodę instancji, którą można zastąpić w teście, i dodaj ją do main.dart.

lib/main.dart

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

Jeśli chcesz, aby nadal działał, musisz wprowadzić niewielkie zmiany w teście. Pełny kod pakietu TestIAPConnection znajdziesz na stronie widget_test.dart na GitHubie.

test/widget_test.dart

void main() {
  testWidgets('App starts', (WidgetTester tester) async {
    IAPConnection.instance = TestIAPConnection();
    await tester.pumpWidget(const MyApp());
    expect(find.text('Tim Sneath'), findsOneWidget);
  });
}

W aplikacji lib/logic/dash_purchases.dart znajdź kod dla: DashPurchases ChangeNotifier. Obecnie do zakupionych kropek możesz dodać tylko DashCounter.

Dodaj usługę subskrypcji strumienia _subscription (typu StreamSubscription<List<PurchaseDetails>> _subscription;), IAPConnection.instance, i importy. Powstały kod powinien wyglądać tak:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

class DashPurchases extends ChangeNotifier {
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter);
}

Słowo kluczowe late zostało dodane do elementu _subscription, ponieważ w konstruktorze zainicjowano element _subscription. Ten projekt jest skonfigurowany tak, aby domyślnie nie dopuszczać do wartości null (NNBD), co oznacza, że właściwości, które nie są zadeklarowane do wartości null, muszą mieć wartość niepustą. Kwalifikator late pozwala opóźnić zdefiniowanie tej wartości.

W konstruktorze pobierz obiekt purchaseUpdatedStream i zacznij słuchać strumienia. W metodzie dispose() anuluj subskrypcję transmisji strumieniowej.

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter) {
    final purchaseUpdated =
        iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  Future<void> buy(PurchasableProduct product) async {
    // omitted
  }

  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }
}

Teraz aplikacja otrzymuje aktualizacje dotyczące zakupu, więc w następnej sekcji dokonasz zakupu.

Zanim przejdziesz dalej, przeprowadź testy na urządzeniu „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 ćwiczenia w Codelabs zastąpisz obecne modele produktów prawdziwymi produktami, które będzie można kupić. Te produkty są wczytywane ze sklepów, wyświetlane na liście i kupowane po kliknięciu produktu.

Dostosowanie PurchasableProduct

PurchasableProduct wyświetla symulowaną usługę. Zaktualizuj ją, tak aby zawierała rzeczywistą treść, zastępując klasę PurchasableProduct w purchasable_product.dart tym kodem:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus {
  purchasable,
  purchased,
  pending,
}

class PurchasableProduct {
  String get id => productDetails.id;
  String get title => productDetails.title;
  String get description => productDetails.description;
  String get price => productDetails.price;
  ProductStatus status;
  ProductDetails productDetails;

  PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

W aplikacji dash_purchases.dart, usuń fikcyjne zakupy i zastąp je pustą listą (List<PurchasableProduct> products = [];)

Wczytaj dostępne zakupy

Aby umożliwić użytkownikowi dokonanie zakupu, wczytaj informacje o zakupach 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. Przy poprzedniej konfiguracji Firebase powinny pojawić się storeKeyConsumable, storeKeySubscription, i storeKeyUpgrade. Jeśli oczekiwany zakup jest niedostępny, wydrukuj te informacje w konsoli. możesz też wysłać te informacje do usługi backendu.

Metoda await iapConnection.queryProductDetails(ids) zwraca zarówno identyfikatory, których nie znaleziono, jak i znalezione produkty, które można kupić. Aby zaktualizować interfejs, użyj pola productDetails w odpowiedzi, a w polu StoreState ustaw wartość available.

lib/logic/dash_purchases.dart

import '../constants.dart';

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

Wywołaj w konstruktorze 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, które można kupić

Weźmy na przykład plik purchase_page.dart. Widżet PurchasePage pokazuje wartości _PurchasesLoading, _PurchaseList, lub _PurchasesNotAvailable, w zależności od wartości StoreState. Widżet pokazuje też wcześniejsze zakupy użytkownika, które zostaną użyte w następnym kroku.

Widżet _PurchaseList pokazuje listę produktów, które można kupić, i wysyła żądanie zakupu do obiektu DashPurchases.

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map((product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              }))
          .toList(),
    );
  }
}

Jeśli dostępne produkty są prawidłowo skonfigurowane, powinny być widoczne w sklepach na Androida i iOS. Pamiętaj, że gdy informacje o zakupach zostaną wprowadzone w odpowiednich konsolach, może minąć trochę czasu.

ca1a9f97c21e552d.png

Wróć do dash_purchases.dart i zaimplementuj funkcję kupowania produktu. Wystarczy oddzielić materiały eksploatacyjne od tych, które się nie zużywają. Przejście na wyższą wersję i usługi objęte subskrypcją nie są produktami zużywanymi.

lib/logic/dash_purchases.dart

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      case storeKeyConsumable:
        await iapConnection.buyConsumable(purchaseParam: purchaseParam);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      default:
        throw ArgumentError.value(
            product.productDetails, '${product.id} is not a known product');
    }
  }

Zanim przejdziesz dalej, utwórz zmienną _beautifiedDashUpgrade i zaktualizuj metodę pobierania beautifiedDash, aby się do niej odwoływał.

lib/logic/dash_purchases.dart

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;

Metoda _onPurchaseUpdate odbiera aktualizacje dotyczące zakupu, aktualizuje stan produktu wyświetlany na stronie zakupu i stosuje zakup do logiki licznika. Po dokonaniu zakupu musisz zadzwonić do sklepu completePurchase, aby poinformować sprzedawcę, że transakcja przebiega 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();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

9. Konfigurowanie backendu

Zanim przejdziesz do śledzenia i weryfikowania zakupów, skonfiguruj backend Dart, aby to ułatwić.

W tej sekcji wykonaj czynności z poziomu folderu dart-backend/ jako katalogu głównego.

Musisz mieć zainstalowane te narzędzia:

Omówienie projektu podstawowego

Ponieważ niektóre części tego projektu nie są objęte tymi ćwiczeniami z programowania, są one uwzględnione w kodzie startowym. Zanim zaczniesz, przejrzyj zawartość kodu startowego, aby zorientować się, jak będziesz budować elementy.

Ten kod backendu może działać lokalnie na Twoim komputerze. Nie musisz go wdrażać, aby z niego korzystać. Musisz jednak mieć możliwość połączenia się z urządzenia z Androidem lub iPhone'a z komputera, na którym będzie działać serwer. W tym celu muszą być w tej samej sieci, a Ty musisz znać adres IP komputera.

Spróbuj uruchomić serwer za pomocą tego polecenia:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Backend Dart używa shelf i shelf_router do obsługi punktów końcowych interfejsu API. Domyślnie serwer nie udostępnia żadnych tras. Później utworzysz trasę do weryfikacji zakupów.

Jedna z części, która jest już uwzględniona w kodzie startowym, to IapRepository w wierszu lib/iap_repository.dart. Ponieważ nauka korzystania z Firestore lub baz danych w ogóle nie jest istotna w przypadku tego ćwiczenia z programowania, kod startowy zawiera funkcje tworzenia lub aktualizowania zakupów w Firestore, a także wszystkie klasy dla tych zakupów.

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, przechodząc do sekcji Konta usługi i wybierając Wygeneruj nowy klucz prywatny.

27590fc77ae94ad4.png

Skopiuj pobrany plik JSON do folderu assets/ i zmień jego nazwę na service-account-firebase.json.

Konfigurowanie dostępu do Google Play

Aby uzyskać dostęp do Sklepu Play na potrzeby weryfikacji zakupów, musisz wygenerować konto usługi z tymi uprawnieniami i pobrać powiązane z nim dane logowania w formacie JSON.

  1. Otwórz Konsolę Google Play i od strony Wszystkie aplikacje.
  2. Kliknij Konfiguracja > Interfejs API. 317fdfb54921f50e.png Jeśli Konsola Google Play poprosi o utworzenie istniejącego projektu lub o połączenie z nim, najpierw zrób to, 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 korzystasz z właściwego konta Google z listy Konto w prawym górnym rogu. 3fb3a25bad803063.png
  6. Po wybraniu projektu kliknij + Utwórz konto usługi na pasku menu u góry. 62fe4c3f8644acd8.png
  7. Podaj nazwę konta usługi (opcjonalnie możesz podać opis, dzięki któremu zapamiętasz, do czego służy), i przejdź do następnego kroku. 8a92d5d6a3dff48c.png
  8. Przypisz do konta usługi rolę Edytujący. 6052b7753667ed1a.png
  9. Zakończ pracę w kreatorze, wróć na stronę Dostęp do interfejsu API w konsoli programisty i kliknij Odśwież konta usługi. Nowo utworzone konto powinno pojawić się na liście. 5895a7db8b4c7659.png
  10. Kliknij Przyznaj dostęp obok nowego konta usługi.
  11. Przewiń następną stronę w dół do bloku Dane finansowe. Wybierz Wyświetlanie danych finansowych, zamówień i odpowiedzi z ankiety na temat anulowania oraz Zarządzaj zamówieniami i subskrypcjami. 75b22d0201cf67e.png
  12. Kliknij Zaproś użytkownika. 70ea0b1288c62a59.png
  13. Teraz, gdy konto jest już skonfigurowane, musisz tylko wygenerować kilka danych logowania. Wróć do konsoli Cloud, znajdź swoje konto usługi na liście kont usługi, kliknij 3 pionowe 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 jeszcze otworzyć plik lib/constants.dart, i zastąpić wartość androidPackageId identyfikatorem pakietu wybranym dla aplikacji na Androida.

Konfigurowanie dostępu do Apple App Store

Aby uzyskać dostęp do App Store na potrzeby weryfikacji zakupów, musisz skonfigurować wspólny tajny klucz:

  1. Otwórz App Store Connect.
  2. Otwórz Moje aplikacje i wybierz swoją aplikację.
  3. Na pasku bocznym wybierz Zakupy w aplikacji > Zarządzaj.
  4. W prawym górnym rogu listy kliknij Wspólny tajny klucz – specyficzny dla aplikacji.
  5. Wygeneruj nowy obiekt tajny i skopiuj go.
  6. Otwórz lib/constants.dart, i zastąp wartość appStoreSharedSecret wybranym przed chwilą udostępnionym tajnym kluczem.

d8b8042470aaeff.png

b72f4565750e2f40.png

Plik konfiguracji stałej

Zanim przejdziesz dalej, upewnij się, że w pliku lib/constants.dart są skonfigurowane te stałe:

  • androidPackageId: identyfikator pakietu używany na Androidzie. np. com.example.dashclicker
  • appStoreSharedSecret: udostępniony tajny klucz umożliwiający dostęp do App Store Connect w celu weryfikacji zakupów.
  • bundleId: identyfikator pakietu używany w iOS. np. com.example.dashclicker

Na razie możesz zignorować pozostałe stałe.

10. Weryfikowanie zakupów

Ogólny proces weryfikacji zakupów jest podobny w iOS i Androidzie.

W przypadku obu sklepów aplikacja otrzymuje token po dokonaniu zakupu.

Aplikacja wysyła token do usługi backendu, która z kolei weryfikuje zakup na serwerach odpowiednich sklepów przy użyciu podanego tokena.

Usługa backendu może następnie zapisać zakup i odpowiedzieć aplikacji, czy zakup był prawidłowy.

Jeśli usługa backendu przeprowadza weryfikację w magazynach, a nie w aplikacji uruchomionej na urządzeniu użytkownika, możesz uniemożliwić użytkownikowi uzyskanie dostępu do funkcji premium, na przykład przez cofnięcie zegara systemowego.

Konfiguracja Flutter

Konfigurowanie uwierzytelniania

Wysyłając informacje o zakupach do usługi backendu, musisz upewnić się, że użytkownik jest uwierzytelniany podczas dokonywania zakupu. Większość zasad uwierzytelniania została już dodana do projektu startowego, musisz tylko upewnić się, że PurchasePage pokazuje przycisk logowania, gdy użytkownik nie jest jeszcze zalogowany. Dodaj ten kod na początku metody kompilacji PurchasePage:

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

class PurchasePage extends StatelessWidget {  
  const PurchasePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }
    // omitted

Punkt końcowy weryfikacji wywołania 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.

Wyślij wybrany sklep (google_play w przypadku Sklepu Play lub app_store w przypadku App Store), serverVerificationData oraz productID. Serwer zwraca kod stanu wskazujący, czy zakup został zweryfikowany.

W wartościach stałych aplikacji skonfiguruj adres IP serwera na adres IP komputera lokalnego.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

  DashPurchases(this.counter, this.firebaseNotifier) {
    // omitted
  }

Dodaj element firebaseNotifier z utworzeniem elementu DashPurchases w: main.dart:

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
          ),
          lazy: false,
        ),

Dodaj w FirebaseNotifier kod pobierający dla użytkownika, aby móc przekazać identyfikator użytkownika do funkcji weryfikacji zakupów.

lib/logic/firebase_notifier.dart

  User? get user => FirebaseAuth.instance.currentUser;

Dodaj funkcję _verifyPurchase do klasy DashPurchases. Ta funkcja async zwraca wartość logiczną wskazującą, czy zakup został zweryfikowany.

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

Wywołaj funkcję _verifyPurchase w _handlePurchase tuż przed dokonaniem zakupu. Zakup należy zastosować tylko wtedy, gdy zostanie zweryfikowany. W aplikacji produkcyjnej możesz to określić dalej, aby na przykład zastosować subskrypcję próbną, gdy sklep jest tymczasowo niedostępny. W tym przykładzie należy to zrobić w prosty sposób i zastosować zakup dopiero wtedy, gdy zostanie zweryfikowany.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      // Send to server
      var validPurchase = await _verifyPurchase(purchaseDetails);

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

W aplikacji są teraz gotowe do weryfikacji zakupów.

Konfigurowanie usługi backendu

Następnie skonfiguruj funkcję w Cloud Functions do weryfikowania zakupów w backendzie.

Tworzenie modułów obsługi zakupów

Ponieważ proces weryfikacji w przypadku obu sklepów jest niemal identyczny, skonfiguruj abstrakcyjną klasę PurchaseHandler z osobnymi implementacjami dla każdego sklepu.

be50c207c5a2a519.png

Zacznij od dodania pliku purchase_handler.dart do folderu lib/, w którym definiujesz abstrakcyjną klasę PurchaseHandler z 2 abstrakcyjnymi metodami weryfikacji 2 różnych rodzajów zakupów: subskrypcji i innych.

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 trzech parametrów:

  • userId: Identyfikator zalogowanego użytkownika. Dzięki temu możesz powiązać z nim zakupy.
  • productData: Dane o produkcie. Definiujesz to za chwilę.
  • token: Token przekazany użytkownikowi przez sklep.

Aby ułatwić sobie korzystanie z tych modułów obsługi zakupów, dodaj metodę verifyPurchase(), która może być używana zarówno w przypadku subskrypcji, jak i pozostałych:

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 będziesz mieć osobną implementację.

Klasa ProductData zawiera podstawowe informacje o różnych produktach do kupienia, w tym ich identyfikator (czasami nazywany też SKU) i ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

  const ProductData(this.productId, this.type);
}

ProductType może być subskrypcją lub nie.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

Lista produktów jest zdefiniowana 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 określ implementacje zastępcze dla Sklepu Google Play i Apple App Store. Zacznij korzystać z Google Play:

Utwórz obiekt lib/google_play_purchase_handler.dart i dodaj klasę, która rozszerza napisane właśnie PurchaseHandler:

lib/google_play_purchase_handler.dart

import 'dart:async';

import 'package:googleapis/androidpublisher/v3.dart' as ap;

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
  );

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }
}

Na razie zwraca true w przypadku metod modułu obsługi. skontaktujesz się z nim później.

Jak możesz zauważyć, konstruktor używa instancji IapRepository. Moduł obsługi zakupów używa tej instancji do przechowywania informacji o zakupach w Firestore na później. Do komunikacji z Google Play służy 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 dodaj klasę, która ponownie stanowi rozszerzenie elementu 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. Masz teraz dwa moduły obsługi zakupów. Teraz utworzymy punkt końcowy interfejsu API weryfikacji zakupów.

Korzystanie z modułów obsługi zakupów

Otwórz bin/server.dart i utwórz punkt końcowy interfejsu 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);
}

({
  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 następujące czynności:

  1. Zdefiniuj punkt końcowy POST, który będzie wywoływany z wcześniej utworzonej aplikacji.
  2. Zdekoduj ładunek JSON i wyodrębnij te informacje:
  3. userId: identyfikator aktualnie zalogowanego użytkownika
  4. source: używany sklep, app_store lub google_play.
  5. productData: uzyskane z utworzonego wcześniej zasobu productDataMap.
  6. token: zawiera dane weryfikacyjne, które należy wysłać do sklepów.
  7. Wywołaj metodę verifyPurchase dla metody GooglePlayPurchaseHandler lub AppStorePurchaseHandler, w zależności od źródła.
  8. Jeśli weryfikacja się udała, metoda zwraca klientowi Response.ok.
  9. Jeśli weryfikacja się nie powiedzie, metoda zwróci klientowi Response.internalServerError.

Po utworzeniu punktu końcowego interfejsu API musisz skonfigurować 2 moduły obsługi zakupu. Wymaga to wczytania kluczy konta usługi uzyskanych w poprzednim kroku i skonfigurowania dostępu do różnych usług, w tym do interfejsów Android Publisher API i Firebase Firestore API. Następnie utwórz dwa moduły 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,
    ),
  };
}

Weryfikowanie zakupów na Androidzie: wdrażanie narzędzia do obsługi zakupów

Następnie kontynuuj wdrażanie modułu obsługi zakupów w Google Play.

Google udostępnia już pakiety DART do interakcji z interfejsami API, które są potrzebne do weryfikacji zakupów. Zainicjowano je w pliku server.dart i teraz używasz ich w klasie GooglePlayPurchaseHandler.

Zaimplementuj moduł obsługi zakupów bez subskrypcji:

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;
  }

Moduł obsługi 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 metodę, która ułatwi Ci analizowanie identyfikatorów zamówień, a także 2 metody analizowania stanu zakupu.

lib/google_play_purchase_handler.dart

/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
  final orderIdSplit = orderId.split('..');
  if (orderIdSplit.isNotEmpty) {
    orderId = orderIdSplit[0];
  }
  return orderId;
}

NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
  return switch (state) {
    0 => NonSubscriptionStatus.completed,
    2 => NonSubscriptionStatus.pending,
    _ => NonSubscriptionStatus.cancelled,
  };
}

SubscriptionStatus _subscriptionStatusFrom(int? state) {
  return switch (state) {
    // Payment pending
    0 => SubscriptionStatus.pending,
    // Payment received
    1 => SubscriptionStatus.active,
    // Free trial
    2 => SubscriptionStatus.active,
    // Pending deferred upgrade/downgrade
    3 => SubscriptionStatus.pending,
    // Expired or cancelled
    _ => SubscriptionStatus.expired,
  };
}

Twoje zakupy w Google Play powinny być teraz zweryfikowane i zapisane w bazie danych.

Następnie przejdź do zakupów w App Store na iOS.

Weryfikowanie zakupów w systemie iOS: zaimplementuj moduł obsługi zakupów

Do weryfikacji zakupów w App Store dostępny jest pakiet Dart innej firmy o nazwie app_store_server_sdk, który ułatwia cały proces.

Zacznij od utworzenia instancji ITunesApi. Użyj konfiguracji piaskownicy i włącz logowanie, aby ułatwić debugowanie błędów.

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(
      ITunesEnvironment.sandbox(),
      loggingEnabled: true,
    ),
  );

Teraz, w przeciwieństwie do interfejsów API Google Play, App Store używa tych samych punktów końcowych dla zarówno subskrypcji, jak i pozostałych subskrypcji. Oznacza to, że możesz używać tej samej logiki w przypadku obu modułów obsługi. Scal je, aby nazywał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) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              status: NonSubscriptionStatus.completed,
            ));
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0')),
              status: SubscriptionStatus.active,
            ));
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

Twoje zakupy w App Store powinny być teraz zweryfikowane i zapisane w bazie danych.

Uruchamianie backendu

W tym momencie możesz uruchomić dart bin/server.dart, aby obsługiwać punkt końcowy /verifypurchase.

$ dart bin/server.dart 
Serving at http://0.0.0.0:8080

11. Monitoruj zakupy

Zalecany sposób śledzenia działań użytkowników są dostępne w usłudze backendu. Wynika to z tego, że backend może reagować na zdarzenia z magazynu, przez co jest mniej podatny na natknięcie się na nieaktualne informacje z powodu pamięci podręcznej. Jest też mniej podatny na modyfikację.

Najpierw skonfiguruj przetwarzanie zdarzeń sklepu w backendzie z wygenerowanym przez siebie backendem Dart.

Przetwarzanie zdarzeń sklepu w backendzie

Sklepy mogą informować Twój backend o wszelkich zdarzeniach rozliczeniowych, na przykład o odnowieniu subskrypcji. Możesz przetwarzać te zdarzenia w backendzie, aby zakupy w bazie danych były aktualne. W tej sekcji skonfiguruj je dla Sklepu Google Play i Apple App Store.

Przetwarzanie zdarzeń płatności w Google Play

Google Play udostępnia zdarzenia rozliczeniowe za pomocą tzw. tematu Cloud Pub/Sub. Są to zasadniczo kolejki wiadomości, w których można publikować i odbierać wiadomości.

Ponieważ jest to funkcja dostępna tylko w Google Play, należy ją uwzględnić w GooglePlayPurchaseHandler.

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 przekaż PubsubApi do GooglePlayPurchaseHandler i zmodyfikuj konstruktor klas, by utworzyć Timer w następujący 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();
    });
  }

Timer jest skonfigurowane do wywoływania metody _pullMessageFromSubSub co 10 sekund. Możesz dostosować Czas trwania.

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 kod komunikuje się z tematem Pub/Sub z Google Cloud co 10 sekund i pyta o nowe wiadomości. Następnie przetwarza wszystkie wiadomości w metodzie _processMessage.

Ta metoda dekoduje wiadomości przychodzące i uzyskuje zaktualizowane informacje o każdym zakupie (zarówno w przypadku subskrypcji, jak i pozostałych subskrypcji), w razie potrzeby wywołuje metodę handleSubscription lub handleNonSubscription.

Każdą wiadomość należy potwierdzić za pomocą metody _askMessage.

Następnie dodaj wymagane zależności do pliku server.dart. Dodaj PubsubApi.cloudPlatformScope do konfiguracji danych logowania:

bin/server.dart

 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 przekaż go do konstruktora GooglePlayPurchaseHandler:

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi, // new
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

Konfiguracja Google Play

Masz już napisany kod do wykorzystywania zdarzeń rozliczeniowych z tematu Pub/Sub, ale nie masz utworzonego tematu Pub/Sub ani nie publikujesz żadnych zdarzeń rozliczeniowych. Czas to skonfigurować.

Najpierw utwórz temat Pub/Sub:

  1. Otwórz stronę Cloud Pub/Sub w konsoli Google Cloud.
  2. Upewnij się, że korzystasz z projektu Firebase, i kliknij + Utwórz temat. d5ebf6897a0a8bf5.png
  3. Nadaj nowemu tematowi nazwę identyczną z wartością ustawioną dla pola GOOGLE_PLAY_PUBSUB_BILLING_TOPIC w polu constants.ts. W tym przypadku nadaj mu nazwę play_billing. Jeśli wybierzesz inną opcję, pamiętaj, by zaktualizować constants.ts. Utwórz temat. 20d690fc543c4212.png
  4. Na liście tematów Pub/Sub kliknij 3 pionowe kropki obok utworzonego tematu, a następnie kliknij Wyświetl uprawnienia. ea03308190609fb.png
  5. Na pasku bocznym po prawej stronie kliknij Dodaj podmiot zabezpieczeń.
  6. W tym miejscu dodaj element google-play-developer-notifications@system.gserviceaccount.com i przypisz mu rolę Publikujący w Pub/Sub. 55631ec0549215bc.png
  7. Zapisz zmiany uprawnień.
  8. Skopiuj Nazwę tematu nowo utworzonego tematu.
  9. Ponownie otwórz Konsolę Play i wybierz aplikację z listy Wszystkie aplikacje.
  10. Przewiń w dół i wybierz Zarabianie >”. Konfiguracja zarabiania.
  11. Wpisz pełny temat i zapisz zmiany. 7e5e875dc6ce5d54.png

Wszystkie zdarzenia płatności w Google Play zostaną opublikowane w tym temacie.

Przetwarzanie zdarzeń rozliczeniowych w App Store

Następnie wykonaj te same czynności w przypadku zdarzeń płatności w App Store. Istnieją 2 efektywne sposoby wdrażania aktualizacji obsługi zakupów w App Store. Jednym z nich jest wdrożenie webhooka, który dostarczasz Apple i którego używa on do komunikacji z Twoim serwerem. Drugim sposobem, który omówiliśmy w tym ćwiczeniu z programowania, jest połączenie się z interfejsem App Store Server API i ręczne uzyskanie informacji o subskrypcji.

W tym ćwiczeniu w Codelabs koncentrujemy się na drugim rozwiązaniu, ponieważ wdrożenie webhooka wymaga udostępnienia serwera w internecie.

W środowisku produkcyjnym najlepiej korzystać z obu tych funkcji. Webhook pobierający zdarzenia z App Store i z interfejsu Server API na wypadek, gdyby ominęło Cię jakieś zdarzenie lub musisz dokładnie sprawdzić stan subskrypcji.

Najpierw otwórz 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ć licznik czasu, który będzie wywoływać metodę _pullStatus. Ten licznik czasu będzie wywoływać metodę _pullStatus co 10 sekund. Możesz dostosować czas trwania minutnika 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 następujący 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 następujący sposób:

  1. Pobiera listę aktywnych subskrypcji z Firestore przy użyciu IapRepository.
  2. W przypadku każdego zamówienia żąda stanu subskrypcji do interfejsu App Store Server API.
  3. Uzyskuje ostatnią transakcję zakupu tej subskrypcji.
  4. Sprawdza datę ważności.
  5. Aktualizuje stan subskrypcji w Firestore. Jeśli wygaśnie, zostanie w ten sposób oznaczona.

Na koniec dodaj cały kod niezbędny do skonfigurowania dostępu do interfejsu App Store Server przez interfejs 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 wybierz Users and Access (Użytkownicy i dostęp).
  2. Wybierz Typ klucza > > Zakup w aplikacji.
  3. Kliknij ikonę plusa. , aby dodać nową.
  4. Wpisz nazwę, np. „Klucz ćwiczeń z programowania”.
  5. Pobierz plik p8 zawierający klucz.
  6. Skopiuj go do folderu zasobów o nazwie SubscriptionKey.p8.
  7. Skopiuj identyfikator klucza z nowo utworzonego klucza i w pliku lib/constants.dart ustaw dla niego stałą appStoreKeyId.
  8. Skopiuj identyfikator wydawcy znajdujący się u góry listy kluczy i ustaw w pliku lib/constants.dart stałą wartość appStoreIssuerId.

9540ea9ada3da151.png

Śledzenie zakupów na urządzeniu

Najbezpieczniejszym sposobem śledzenia zakupów jest po stronie serwera, ponieważ trudno jest zabezpieczyć klienta, ale potrzebny jest sposób przekazywania informacji klientowi, tak aby aplikacja mogła wykorzystać informacje o stanie subskrypcji. Przechowując zakupy w Firestore, możesz łatwo synchronizować dane z klientem i aktualizować je automatycznie.

Zasób IAPRepo został już uwzględniony w aplikacji, która jest repozytorium Firestore, które zawiera wszystkie dane dotyczące zakupów użytkownika w List<PastPurchase> purchases. Repozytorium zawiera też zasadę hasActiveSubscription,, która ma wartość true (prawda), gdy zakup dokonany za pomocą productId storeKeySubscription ma stan, który nie wygasł. Jeśli 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, w której należy zastosować lub usunąć subskrypcje. Dodaj więc iapRepo jako właściwość do klasy i przypisz iapRepo w konstruktorze. Następnie dodaj detektor bezpośrednio w konstruktorze i usuń go w metodzie dispose(). Początkowo odbiornik może być tylko pustą funkcją. Ponieważ IAPRepo to ChangeNotifier i wywołujesz notifyListeners() przy każdej zmianie zakupów w Firestore, metoda purchasesUpdate() jest zawsze wywoływana w przypadku zmiany kupionych produktów.

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

  DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
    final purchaseUpdated =
        iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    iapRepo.addListener(purchasesUpdate);
    loadPurchases();
  }

  @override
  void dispose() {
    iapRepo.removeListener(purchasesUpdate);
    _subscription.cancel();
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

Następnie przekaż IAPRepo do konstruktora w main.dart.. Możesz pobrać repozytorium za pomocą 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 ustawiają mnożnik odpowiednio na 10 lub 1, więc nie musisz sprawdzać, czy subskrypcja została już zastosowana. Gdy zmieni się stan subskrypcji, aktualizujesz też stan produktu, który można kupić, dzięki czemu będziesz widzieć na stronie zakupu, że jest już aktywny. Ustaw właściwość _beautifiedDashUpgrade w zależności od tego, czy kupiono uaktualnienie.

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();
    }
  }

Masz teraz pewność, że stan subskrypcji i licencji w usłudze backendu jest zawsze aktualny i zsynchronizowany z aplikacją. Aplikacja działa w odpowiedni sposób i stosuje funkcje subskrypcji oraz uaktualnienia w grze Dasher.

12. Wszystko gotowe

Gratulacje! Ćwiczenie z programowania zostało ukończone. Ukończony kod tego ćwiczenia z programowania znajdziesz w android_studio_folder.pngpełnym folderze.

Aby dowiedzieć się więcej, wykonaj inne ćwiczenia z programowania Flutter.