Dodawanie zakupów w aplikacji do aplikacji Flutter

1. Wprowadzenie

Dodanie zakupów w aplikacji do aplikacji Flutter wymaga prawidłowego skonfigurowania sklepów z aplikacjami, zweryfikowania zakupu i przyznania niezbędnych uprawnień, takich jak korzyści z subskrypcji.

W tym laboratorium kodowania dodasz do aplikacji (która jest dostępna) 3 rodzaje zakupów w aplikacji i zweryfikujesz je za pomocą backendu Dart z Firebase. Dostarczona aplikacja Dash Clicker zawiera grę, w której maskotka Dash jest używana jako waluta. Dodasz te opcje zakupu:

  1. Opcja zakupu 2000 Dashes naraz, którą można powtarzać.
  2. Jednorazowy zakup uaktualnienia, które zmieni stary styl Dash na nowoczesny.
  3. Subskrypcja, która podwaja automatycznie generowane kliknięcia.

Pierwsza opcja zakupu daje użytkownikowi bezpośrednią korzyść w postaci 2000 Dashes. Są one bezpośrednio dostępne dla użytkownika i można je kupić wiele razy. Jest to produkt konsumpcyjny, ponieważ jest bezpośrednio konsumowany i może być konsumowany wielokrotnie.

Druga opcja to ulepszenie Dash do bardziej atrakcyjnej wersji. Wystarczy kupić go raz, a będzie dostępny na zawsze. Taki zakup nazywa się niekonsumpcyjnym, ponieważ nie może być wykorzystany przez aplikację, ale jest ważny na zawsze.

Trzecią i ostatnią opcją zakupu jest subskrypcja. Gdy subskrypcja jest aktywna, użytkownik szybciej otrzymuje Dash, ale gdy przestanie za nią płacić, korzyści również znikną.

Usługa backendu (również udostępniona) działa jako aplikacja w Dart, weryfikuje zakupy i przechowuje je za pomocą Firestore. Aby ułatwić ten proces, używamy Firestore, ale w aplikacji produkcyjnej możesz użyć dowolnego typu usługi backendu.

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

Co utworzysz

  • Rozszerzysz aplikację, aby obsługiwała zakupy jednorazowe i subskrypcje.
  • Rozszerzysz też aplikację backendu Dart, aby weryfikować i przechowywać kupione produkty.

Czego się nauczysz

  • Jak skonfigurować App Store i Sklep Play z produktami, które można kupić.
  • Jak komunikować się ze sklepami, aby weryfikować zakupy i przechowywać je w Firestore.
  • Jak zarządzać zakupami w aplikacji.

Czego potrzebujesz

2. Konfigurowanie środowiska programistycznego

Aby rozpocząć ten kurs, pobierz kod i zmień identyfikator pakietu na iOS oraz nazwę pakietu na Androidzie.

Pobieranie kodu

Aby sklonować repozytorium GitHub z wiersza poleceń, użyj tego polecenia:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

Jeśli masz zainstalowane narzędzie cli GitHuba, użyj tego polecenia:

gh repo clone flutter/codelabs flutter-codelabs

Przykładowy kod zostanie sklonowany do katalogu flutter-codelabs, który zawiera kod kolekcji codelabów. Kod do tego ćwiczenia jest w języku flutter-codelabs/in_app_purchases.

Struktura katalogów w flutter-codelabs/in_app_purchases zawiera serię zrzutów stanu, w którym powinna znajdować się aplikacja po wykonaniu każdego z kroków. Kod początkowy znajduje się w kroku 0, więc przejdź do niego w ten sposób:

cd flutter-codelabs/in_app_purchases/step_00

Jeśli chcesz przejść do przodu lub zobaczyć, jak coś powinno wyglądać po wykonaniu danego kroku, zajrzyj do katalogu o nazwie odpowiadającej temu krokowi. Kod ostatniego kroku znajduje się w folderze complete.

Konfigurowanie projektu startowego

Otwórz projekt początkowy z step_00/app w ulubionym środowisku IDE. Do zrobienia zrzutów ekranu użyliśmy Androida Studio, ale Visual Studio Code to też świetna opcja. W obu edytorach sprawdź, czy są zainstalowane najnowsze wtyczki Dart i Flutter.

Aplikacje, które zamierzasz utworzyć, muszą komunikować się z App Store i Google Play, aby wiedzieć, które produkty są dostępne i w jakiej cenie. Każda aplikacja jest identyfikowana za pomocą unikalnego identyfikatora. W przypadku sklepu App Store na iOS jest to identyfikator pakietu, a w przypadku sklepu Google Play na Androida – identyfikator aplikacji. Te identyfikatory są zwykle tworzone przy użyciu notacji z odwróconą nazwą domeny. Na przykład podczas tworzenia aplikacji do zakupu w aplikacji na flutter.dev użyjesz dev.flutter.inapppurchase. Wymyśl identyfikator aplikacji, który teraz ustawisz w ustawieniach projektu.

Najpierw skonfiguruj identyfikator pakietu na iOS. Aby to zrobić, otwórz plik Runner.xcworkspace w aplikacji Xcode.

a9fbac80a31e28e0.png

W strukturze folderów Xcode na górze znajduje się projekt Runner, a pod nim są elementy docelowe Flutter, RunnerProducts. Kliknij dwukrotnie Runner, aby edytować ustawienia projektu, a następnie kliknij Signing & Capabilities (Podpisywanie i możliwości). Wpisz wybrany identyfikator pakietu w polu Zespół, aby ustawić zespół.

812f919d965c649a.jpeg

Możesz teraz zamknąć Xcode i wrócić do Android Studio, aby dokończyć konfigurację na potrzeby Androida. Aby to zrobić, otwórz plik build.gradle.kts w sekcji android/app, i zmień applicationId (w wierszu 24 na zrzucie ekranu poniżej) na identyfikator aplikacji, taki sam jak identyfikator pakietu iOS. Pamiętaj, że identyfikatory sklepów na iOS i Androida nie muszą być identyczne, ale ich identyczność zmniejsza ryzyko błędów, dlatego w tym samouczku będziemy używać identycznych identyfikatorów.

e320a49ff2068ac2.png

3. Instalowanie wtyczki

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

Dodawanie zależności w pliku pubspec

Dodaj in_app_purchase do pliku pubspec, dodając in_app_purchase do zależności w projekcie:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Otwórz plik pubspec.yaml i sprawdź, czy w sekcji dependencies jest teraz wpis in_app_purchase, a w sekcji dev_dependencies – wpis in_app_purchase_platform_interface.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  in_app_purchase_platform_interface: ^1.4.0

4. Konfigurowanie App Store

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

Aby korzystać z zakupów w aplikacji, musisz też mieć aktywną umowę dotyczącą płatnych aplikacji w App Store Connect. Otwórz stronę https://appstoreconnect.apple.com/ i kliknij Umowy, podatki i bankowość.

11db9fca823e7608.png

Znajdziesz tu umowy dotyczące aplikacji bezpłatnych i płatnych. Stan aplikacji bezpłatnych powinien być aktywny, a stan aplikacji płatnych to „nowa”. Zapoznaj się z warunkami, zaakceptuj je i podaj wszystkie wymagane informacje.

74c73197472c9aec.png

Jeśli wszystko jest skonfigurowane prawidłowo, stan płatnych aplikacji będzie aktywny. Jest to bardzo ważne, ponieważ bez aktywnej umowy nie będziesz mieć możliwości wypróbowania zakupów w aplikacji.

4a100bbb8cafdbbf.jpeg

Rejestrowanie identyfikatora aplikacji

Utwórz nowy identyfikator w portalu Apple Developer. Otwórz stronę developer.apple.com/account/resources/identifiers/list i kliknij ikonę „+” obok nagłówka Identyfikatory.

55d7e592d9a3fc7b.png

Wybierz identyfikatory aplikacji

13f125598b72ca77.png

Wybierz aplikację

41ac4c13404e2526.png

Podaj opis i ustaw identyfikator pakietu tak, aby był zgodny z wartością ustawioną wcześniej w XCode.

9d2c940ad80deeef.png

Więcej wskazówek na temat tworzenia nowego identyfikatora aplikacji znajdziesz w pomocy dotyczącej konta dewelopera.

Tworzenie nowej aplikacji

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

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 środowisku piaskownicy. Ten użytkownik testowy nie powinien być połączony z iTunes – służy on tylko do testowania zakupów w aplikacji. Nie możesz użyć adresu e-mail, który jest już używany na koncie Apple. W sekcji Użytkownicy i dostęp kliknij Sandbox, aby utworzyć nowe konto testowe lub zarządzać dotychczasowymi identyfikatorami Apple ID w środowisku testowym.

2ba0f599bcac9b36.png

Teraz możesz skonfigurować użytkownika piaskownicy na iPhonie, klikając Ustawienia > Deweloper > Konto Apple w piaskownicy.

74a545210b282ad8.png eaa67752f2350f74.png

Konfigurowanie zakupów w aplikacji

Teraz skonfiguruj 3 produkty, które można kupić:

  • dash_consumable_2k: zakup konsumpcyjny, który można kupić wiele razy i który przyznaje użytkownikowi 2000 kreski (waluta w aplikacji) za każdy zakup.
  • dash_upgrade_3d: niepodlegający konsumpcji zakup „ulepszenia”, którego można dokonać tylko raz i który daje użytkownikowi możliwość kliknięcia Dasha o odmiennym wyglądzie.
  • dash_subscription_doubler: subskrypcja, która przez cały okres jej trwania przyznaje użytkownikowi 2 razy więcej Dashów za kliknięcie.

a118161fac83815a.png

Kliknij Zakupy w aplikacji.

Utwórz zakupy w aplikacji z określonymi identyfikatorami:

  1. Skonfiguruj dash_consumable_2k jako materiał eksploatacyjny. Użyj dash_consumable_2k jako identyfikatora produktu. Nazwa referencyjna jest używana tylko w App Store Connect. Ustaw ją na dash consumable 2k. 1f8527fc03902099.png Skonfiguruj dostępność. Produkt musi być dostępny w kraju użytkownika piaskownicy. bd6b2ce2d9314e6e.png Dodaj cenę i ustaw ją na $1.99 lub równowartość w innej walucie. 926b03544ae044c4.png Dodaj lokalizacje dla zakupu. Wywołaj zdarzenie zakupu Spring is in the air z opisem 2000 dashes fly out. e26dd4f966dcfece.png Dodaj zrzut ekranu opinii. Treść nie ma znaczenia, dopóki produkt nie zostanie wysłany do sprawdzenia, ale jest wymagana, aby produkt był w stanie „Gotowy do przesłania”, co jest konieczne, gdy aplikacja pobiera produkty z App Store. 25171bfd6f3a033a.png
  2. Skonfiguruj dash_upgrade_3d jako produkt niekonsumpcyjny. Użyj dash_upgrade_3d jako identyfikatora produktu. Ustaw nazwę odwołania na dash upgrade 3d. Wywołaj zdarzenie zakupu 3D Dash z opisem Brings your dash back to the future. Ustaw cenę na $0.99. Skonfiguruj dostępność i prześlij zrzut ekranu z opinią w taki sam sposób jak w przypadku dash_consumable_2kproduktu83878759f32a7d4a.png.
  3. Skonfiguruj dash_subscription_doubler jako automatycznie odnawianą subskrypcję. Proces subskrypcji wygląda nieco inaczej. Najpierw musisz utworzyć grupę subskrypcji. Jeśli kilka subskrypcji należy do tej samej grupy, użytkownik może subskrybować tylko jedną z nich w danym momencie, ale może przejść na wyższą lub niższą wersję subskrypcji. Zadzwoń do grupy subscriptions. 393a44b09f3cd8bf.png Dodaj lokalizację dla grupy subskrypcji. 595aa910776349bd.png Następnie utworzysz subskrypcję. Ustaw nazwę odniesienia na dash subscription doubler, a identyfikator produktu na dash_subscription_doubler. 7bfff7bbe11c8eec.png Następnie wybierz czas trwania subskrypcji (1 tydzień) i lokalizacje. Nazwij tę subskrypcję Jet Engine i dodaj opis Doubles your clicks. Ustaw cenę na $0.49. Skonfiguruj dostępność i prześlij zrzut ekranu z opinią w taki sam sposób jak w przypadku dash_consumable_2kproduktu44d18e02b926a334.png.

Produkty powinny być teraz widoczne na listach:

17f242b5c1426b79.png d71da951f595054a.png

5. Konfigurowanie Sklepu Play

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

Tworzenie nowej aplikacji

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

  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. Określ, że Twoja aplikacja jest grą. Możesz to później zmienić.
  5. Określ, czy aplikacja ma być bezpłatna czy płatna.
  6. Wypełnij deklaracje „Wytyczne dotyczące treści” i „Przepisy eksportowe USA”.
  7. Kliknij Utwórz aplikację.

Po utworzeniu aplikacji przejdź do panelu i wykonaj wszystkie zadania w sekcji Skonfiguruj aplikację. W tym miejscu podajesz informacje o aplikacji, takie jak oceny treści i zrzuty ekranu. 13845badcf9bc1db.png

Podpisywanie aplikacji

Aby testować zakupy w aplikacji, musisz przesłać do Google Play co najmniej jedną wersję.

W tym celu musisz podpisać kompilację wersji czymś innym niż klucze debugowania.

Tworzenie magazynu kluczy

Jeśli masz już magazyn kluczy, przejdź do następnego kroku. Jeśli nie, utwórz go, uruchamiając to polecenie w wierszu poleceń.

Na komputerze Mac lub z Linuksem użyj tego polecenia:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

W systemie Windows użyj tego polecenia:

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

To polecenie zapisuje plik key.jks w katalogu domowym. Jeśli chcesz zapisać plik w innym miejscu, zmień argument przekazywany do parametru -keystore. Zachowaj

keystore

plik prywatny; nie sprawdzaj go w publicznym systemie kontroli wersji.

Odwołanie do magazynu kluczy z aplikacji

Utwórz plik o nazwie <your app dir>/android/key.properties, który zawiera odwołanie do magazynu kluczy:

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>

Konfigurowanie podpisywania w Gradle

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

Dodaj informacje o magazynie kluczy z pliku właściwości przed blokiem android:

import java.util.Properties
import java.io.FileInputStream

plugins {
    // omitted
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // omitted
}

Wczytaj plik key.properties do obiektu keystoreProperties.

Zaktualizuj blok buildTypes, aby:

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Skonfiguruj blok signingConfigs w pliku build.gradle.kts modułu, podając informacje o konfiguracji podpisywania:

   signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Wersje aplikacji do publikacji będą teraz podpisywane automatycznie.

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

Przesyłanie pierwszej wersji

Po skonfigurowaniu aplikacji do podpisywania możesz ją skompilować, uruchamiając to polecenie:

flutter build appbundle

To polecenie domyślnie generuje wersję do wdrożenia, a dane wyjściowe można znaleźć w lokalizacji <your app dir>/build/app/outputs/bundle/release/.

Na panelu w Konsoli Google Play kliknij Testuj i publikuj > Testowanie > Test zamknięty i utwórz nową wersję testu zamkniętego.

Następnie prześlij pakiet aplikacji app-release.aab wygenerowany przez polecenie kompilacji.

Kliknij Zapisz, a potem Sprawdź wersję.

Na koniec kliknij Rozpocznij wdrażanie w ramach testów zamkniętych, aby aktywować wersję do testów zamkniętych.

Konfigurowanie użytkowników testowych

Aby można było testować zakupy w aplikacji, konta Google testerów muszą być dodane w Konsoli Google Play w 2 miejscach:

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

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

a0d0394e85128f84.png

Utwórz nową listę e-mailową, klikając Utwórz listę e-mailową. Nadaj liście nazwę i dodaj adresy e-mail kont Google, które potrzebują dostępu do testowania zakupów w aplikacji.

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

Następnie dodaj testerów licencji:

  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 muszą mieć możliwość testowania zakupów w aplikacji.
  4. Ustaw Odpowiedź licencji na RESPOND_NORMALLY.
  5. Kliknij Zapisz zmiany.

a1a0f9d3e55ea8da.png

Konfigurowanie zakupów w aplikacji

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

Podobnie jak w App Store musisz zdefiniować 3 różne zakupy:

  • dash_consumable_2k: zakup konsumpcyjny, który można kupić wiele razy i który przyznaje użytkownikowi 2000 kreski (waluta w aplikacji) za każdy zakup.
  • dash_upgrade_3d: jednorazowy zakup „ulepszenia”, które daje użytkownikowi możliwość klikania Dasha o odmiennym wyglądzie.
  • dash_subscription_doubler: subskrypcja, która przez cały okres jej trwania przyznaje użytkownikowi 2 razy więcej Dashów za kliknięcie.

Najpierw dodaj produkty konsumpcyjne i niekonsumpcyjne.

  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 dokładnie taki sam, jak identyfikator, którego chcesz użyć.
  5. Kliknij Zapisz.
  6. Kliknij Aktywuj.
  7. Powtórz ten proces w przypadku zakupu „ulepszenia” niepodlegającego konsumpcji.

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 o subskrypcji. Upewnij się, że identyfikator produktu jest dokładnie taki sam, jak identyfikator, którego chcesz użyć.
  5. Kliknij Zapisz.

Zakupy powinny być teraz skonfigurowane w Konsoli Play.

6. Konfigurowanie Firebase

W tym module dowiesz się, jak używać usługi backendu do weryfikowania i śledzenia zakupów użytkowników.

Korzystanie z usługi backendu ma kilka zalet:

  • Możesz bezpiecznie weryfikować transakcje.
  • Możesz reagować na zdarzenia związane z płatnościami w sklepach z aplikacjami.
  • Zakupy możesz śledzić w bazie danych.
  • Użytkownicy nie będą mogli oszukać aplikacji, aby uzyskać dostęp do funkcji premium, cofając zegar systemowy.

Usługę backendu można skonfigurować na wiele sposobów, ale w tym przypadku użyjesz funkcji w chmurze i Firestore w ramach Firebase od Google.

Pisanie backendu wykracza poza zakres tego laboratorium, więc kod początkowy zawiera już projekt Firebase, który obsługuje podstawowe zakupy, aby ułatwić Ci rozpoczęcie pracy.

Aplikacja startowa zawiera też wtyczki Firebase.

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

Tworzenie projektu Firebase

Otwórz konsolę Firebase i utwórz nowy projekt Firebase. W tym przykładzie nazwij projekt Dash Clicker.

W aplikacji backendowej wiążesz zakupy z określonym użytkownikiem, dlatego musisz uwierzytelniać użytkowników. W tym celu użyj modułu uwierzytelniania Firebase z logowaniem przez Google.

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

fe2e0933d6810888.png

Będziesz też używać bazy danych Firestore w Firebase, więc włącz również tę usługę.

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

Zalecany sposób instalowania Firebase w aplikacji Flutter to użycie wiersza poleceń FlutterFire. Postępuj zgodnie z instrukcjami na stronie konfiguracji.

Podczas uruchamiania polecenia flutterfire configure wybierz projekt utworzony w poprzednim kroku.

$ flutterfire configure

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

Następnie włącz iOSAndroid, wybierając te 2 platformy.

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

Gdy pojawi się pytanie o zastąpienie pliku firebase_options.dart, wybierz „yes” (tak).

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

Konfigurowanie Firebase na Androidzie: dalsze kroki

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

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

b22d46a759c0c834.png

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

Pobieranie haszu certyfikatu podpisywania debugowania

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

cd android
./gradlew :app:signingReport

Pojawi się długa lista kluczy podpisywania. Szukasz skrótu certyfikatu debugowania, więc znajdź certyfikat, w którym właściwości VariantConfig mają wartość debug. Plik keystore prawdopodobnie znajduje się w folderze domowym w katalogu .android/debug.keystore.

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

Skopiuj skrót SHA-1 i wypełnij ostatnie pole w oknie przesyłania aplikacji.

Na koniec ponownie uruchom flutterfire configure, aby zaktualizować aplikację i uwzględnić konfigurację podpisywania.

$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes

Konfigurowanie Firebase na iOS: dalsze kroki

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

W VSCode kliknij prawym przyciskiem myszy folder ios/, a następnie kliknij open in xcode.

W Android Studio kliknij prawym przyciskiem myszy folder ios/, a potem kliknij kolejno flutteropen iOS module in Xcode.

Aby umożliwić logowanie przez Google na urządzeniach z iOS, dodaj opcję konfiguracji CFBundleURLTypes do plików kompilacji plist. (Więcej informacji znajdziesz w dokumentacji google_sign_inpakietu). W tym przypadku plik to ios/Runner/Info.plist.

Para klucz-wartość została już dodana, ale jej wartości muszą zostać zastąpione:

  1. Pobierz wartość REVERSED_CLIENT_ID z pliku GoogleService-Info.plist bez otaczającego ją elementu <string>..</string>.
  2. Zastąp wartość w pliku ios/Runner/Info.plist pod kluczem 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. Odsłuchiwanie aktualizacji dotyczących zakupów

W tej części ćwiczenia przygotujesz aplikację do zakupu produktów. Proces ten obejmuje nasłuchiwanie aktualizacji i błędów zakupu po uruchomieniu aplikacji.

Słuchanie aktualizacji dotyczących zakupów

main.dart, znajdź widżet MyHomePage, który ma ScaffoldBottomNavigationBar zawierającym 2 strony. Ta strona tworzy też 3 Provider dla DashCounter, DashUpgrades,DashPurchases. DashCounter śledzi bieżącą liczbę kresek i automatycznie ją zwiększa. DashUpgrades zarządza ulepszeniami, które możesz kupić za Dash. Ten Codelab dotyczy DashPurchases.

Domyślnie obiekt dostawcy jest definiowany, gdy po raz pierwszy zostanie o niego wysłane żądanie. Ten obiekt nasłuchuje aktualizacji zakupu bezpośrednio po uruchomieniu aplikacji, więc wyłącz na nim leniwe wczytywanie za pomocą tego kodu: lazy: false

lib/main.dart

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

Potrzebujesz też instancji elementu InAppPurchaseConnection. Aby jednak można było testować aplikację, musisz mieć możliwość symulowania połączenia. Aby to zrobić, utwórz metodę instancji, którą można zastąpić w teście, i dodaj ją do main.dart.

lib/main.dart

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

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

Zaktualizuj test w ten sposób:

test/widget_test.dart

import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import

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

class TestIAPConnection implements InAppPurchase {         // Add from here
  @override
  Future<bool> buyConsumable({
    required PurchaseParam purchaseParam,
    bool autoConsume = true,
  }) {
    return Future.value(false);
  }

  @override
  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
    return Future.value(false);
  }

  @override
  Future<void> completePurchase(PurchaseDetails purchase) {
    return Future.value();
  }

  @override
  Future<bool> isAvailable() {
    return Future.value(false);
  }

  @override
  Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
    return Future.value(
      ProductDetailsResponse(productDetails: [], notFoundIDs: []),
    );
  }

  @override
  T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
    // TODO: implement getPlatformAddition
    throw UnimplementedError();
  }

  @override
  Stream<List<PurchaseDetails>> get purchaseStream =>
      Stream.value(<PurchaseDetails>[]);

  @override
  Future<void> restorePurchases({String? applicationUserName}) {
    // TODO: implement restorePurchases
    throw UnimplementedError();
  }

  @override
  Future<String> countryCode() {
    // TODO: implement countryCode
    throw UnimplementedError();
  }
}                                                          // To here.

W lib/logic/dash_purchases.dart przejdź do kodu DashPurchasesChangeNotifier. Obecnie do kupionych Dashy możesz dodać tylko DashCounter.

Dodaj właściwość subskrypcji strumienia _subscription (typu StreamSubscription<List<PurchaseDetails>> _subscription;), IAPConnection.instance, i importy. Wynikowy kod powinien wyglądać tak:

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';           // Add this import

import '../main.dart';                                           // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.available;
  late StreamSubscription<List<PurchaseDetails>> _subscription;  // Add this line
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;                  // And this line

  DashPurchases(this.counter);

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
}

Słowo kluczowe late jest dodawane do _subscription, ponieważ _subscription jest inicjowane w konstruktorze. Ten projekt jest domyślnie skonfigurowany jako niepusty (NNBD), co oznacza, że właściwości, które nie są zadeklarowane jako dopuszczające wartość null, muszą mieć wartość inną niż null. Kwalifikator late umożliwia opóźnienie określenia tej wartości.

W konstruktorze pobierz strumień purchaseUpdated i zacznij go słuchać. W przypadku metody dispose() anuluj subskrypcję strumieniowania.

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

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

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

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

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

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

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

Zanim przejdziesz dalej, uruchom testy z flutter test", aby sprawdzić, czy wszystko jest prawidłowo skonfigurowane.

$ flutter test

00:01 +1: All tests passed!

8. dokonywać zakupów;

W tej części laboratorium kodowania zastąpisz istniejące produkty testowe prawdziwymi produktami, które można kupić. Te produkty są wczytywane ze sklepów, wyświetlane na liście i kupowane po kliknięciu produktu.

Adapt PurchasableProduct

PurchasableProduct wyświetla przykładowy produkt. Zaktualizuj go, aby wyświetlał rzeczywistą treść, zastępując klasę PurchasableProductpurchasable_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ń zakupy testowe i zastąp je pustą listą List<PurchasableProduct> products = [];.

Wczytaj dostępne zakupy

Aby umożliwić użytkownikowi dokonanie zakupu, wczytaj zakupy ze sklepu. Najpierw sprawdź, czy sklep jest dostępny. Gdy sklep jest niedostępny, ustawienie storeState na notAvailable powoduje wyświetlenie użytkownikowi komunikatu o błędzie.

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

Gdy sklep będzie dostępny, wczytaj dostępne zakupy. W przypadku poprzedniej konfiguracji Google Play i App Store spodziewaj się zobaczyć storeKeyConsumable, storeKeySubscription,storeKeyUpgrade. Gdy oczekiwany zakup nie jest dostępny, wydrukuj te informacje w konsoli. Możesz też wysłać je do usługi backendu.

Metoda await iapConnection.queryProductDetails(ids) zwraca zarówno identyfikatory, których nie udało się znaleźć, jak i produkty, które można kupić. Użyj productDetails z odpowiedzi, aby zaktualizować interfejs, i ustaw StoreState na available.

lib/logic/dash_purchases.dart

import '../constants.dart';

// ...

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    products = response.productDetails
        .map((e) => PurchasableProduct(e))
        .toList();
    storeState = StoreState.available;
    notifyListeners();
  }

Wywołaj funkcję loadPurchases() w konstruktorze:

lib/logic/dash_purchases.dart

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

Na koniec zmień wartość pola storeStateStoreState.available na StoreState.loading:.

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Wyświetlanie produktów, które można kupić

Weź pod uwagę plik purchase_page.dart. Widżet PurchasePage wyświetla _PurchasesLoading, _PurchaseList, lub _PurchasesNotAvailable, w zależności od StoreState. Widżet wyświetla też poprzednie zakupy użytkownika, które są wykorzystywane w następnym kroku.

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

lib/pages/purchase_page.dart

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

Jeśli produkty są prawidłowo skonfigurowane, powinny być widoczne w sklepach na Androida i iOS. Pamiętaj, że może minąć trochę czasu, zanim zakupy będą dostępne po wprowadzeniu ich do odpowiednich konsol.

ca1a9f97c21e552d.png

Wróć do dash_purchases.dart i wdroż funkcję zakupu produktu. Wystarczy oddzielić materiały eksploatacyjne od nieeksploatacyjnych. Ulepszenie i produkty subskrypcyjne są produktami niekonsumpcyjnymi.

lib/logic/dash_purchases.dart

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

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

lib/logic/dash_purchases.dart

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

Metoda _onPurchaseUpdate otrzymuje aktualizacje zakupu, aktualizuje stan produktu wyświetlanego na stronie zakupu i stosuje zakup do logiki licznika. Po obsłużeniu zakupu ważne jest wywołanie funkcji completePurchase, aby sklep wiedział, że zakup został prawidłowo obsłużony.

lib/logic/dash_purchases.dart

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

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

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

9. Konfigurowanie backendu

Zanim przejdziesz do śledzenia i weryfikowania zakupów, skonfiguruj backend Dart, aby to umożliwić.

W tej sekcji pracuj w folderze dart-backend/ jako folderze głównym.

Sprawdź, czy masz zainstalowane te narzędzia:

Omówienie projektu podstawowego

Niektóre części tego projektu są uważane za wykraczające poza zakres tego laboratorium, dlatego są uwzględnione w kodzie początkowym. Zanim zaczniesz, warto przejrzeć kod początkowy, aby zorientować się, jak będzie wyglądać struktura projektu.

Ten kod backendu może działać lokalnie na Twoim komputerze, więc nie musisz go wdrażać, aby go używać. Musisz jednak mieć możliwość połączenia urządzenia deweloperskiego (Androida lub iPhone’a) z komputerem, na którym będzie działać serwer. W tym celu muszą być w tej samej sieci, a Ty musisz znać adres IP swojego urządzenia.

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

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Backend Dart korzysta z 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.

Jednym z elementów, który jest już uwzględniony w kodzie początkowym, jest IapRepositorylib/iap_repository.dart. Ponieważ nauka interakcji z Firestore lub bazami danych w ogóle nie jest uważana za istotną w tym laboratorium, kod początkowy zawiera funkcje tworzenia i aktualizowania zakupów w Firestore, a także wszystkie klasy tych zakupów.

Konfigurowanie dostępu do Firebase

Aby uzyskać dostęp do Firebase Firestore, potrzebujesz klucza dostępu do konta usługi. Wygeneruj go, otwierając ustawienia projektu Firebase i przechodząc do sekcji Konta usługi, a następnie wybierając Wygeneruj nowy klucz prywatny.

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 w celu weryfikacji zakupów, musisz wygenerować konto usługi z tymi uprawnieniami i pobrać dla niego dane logowania w formacie JSON.

  1. Otwórz stronę interfejsu Google Play Android Developer API w Google Cloud Console. 629f0bd8e6b50be8.png Jeśli Konsola Google Play poprosi Cię o utworzenie projektu lub połączenie go z istniejącym projektem, najpierw wykonaj tę czynność, a potem wróć na tę stronę.
  2. Następnie otwórz stronę Konta usługi i kliknij + Utwórz konto usługi. 8dc97e3b1262328a.png
  3. Wpisz nazwę konta usługi i kliknij Utwórz i kontynuuj. 4fe8106af85ce75f.png
  4. Wybierz rolę Subskrybent Pub/Sub i kliknij Gotowe. a5b6fa6ea8ee22d.png
  5. Po utworzeniu konta kliknij Zarządzaj kluczami. eb36da2c1ad6dd06.png
  6. Kliknij Dodaj klucz > Utwórz nowy klucz. e92db9557a28a479.png
  7. Utwórz i pobierz klucz JSON. 711d04f2f4176333.png
  8. Zmień nazwę pobranego pliku na service-account-google-play.json, i przenieś go do katalogu assets/.
  9. Następnie otwórz stronę Użytkownicy i uprawnieniaKonsoli Play28fffbfc35b45f97.png
  10. Kliknij Zaproś nowych użytkowników i wpisz adres e-mail utworzonego wcześniej konta usługi. Adres e-mail znajdziesz w tabeli na stronie Konta usługie3310cc077f397d.png
  11. Przyznaj aplikacji uprawnienia Wyświetlanie danych finansowychZarządzanie zamówieniami i subskrypcjami. a3b8cf2b660d1900.png
  12. Kliknij Zaproś użytkownika.

Musimy jeszcze otworzyć plik lib/constants.dart, i zastąpić wartość androidPackageId identyfikatorem pakietu wybranym dla aplikacji na Androida.

Konfigurowanie dostępu do Apple App Store

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

  1. Otwórz App Store Connect.
  2. Otwórz Moje aplikacje i wybierz aplikację.
  3. W menu bocznym kliknij Ogólne > Informacje o aplikacji.
  4. W sekcji App-Specific Shared Secret (Wspólny tajny klucz aplikacji) kliknij Manage (Zarządzaj). ad419782c5fbacb2.png
  5. Wygeneruj nowy klucz tajny i go skopiuj. b5b72a357459b0e5.png
  6. Otwórz plik lib/constants.dart, i zastąp wartość appStoreSharedSecret wygenerowanym przed chwilą tajnym kluczem.

Plik konfiguracji stałych

Zanim przejdziesz dalej, sprawdź, czy w pliku lib/constants.dart skonfigurowano te stałe:

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

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

10. Weryfikowanie zakupów

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

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

Ten token jest wysyłany przez aplikację do usługi backendu, która następnie weryfikuje zakup na serwerach odpowiedniego sklepu za pomocą podanego tokena.

Usługa backendu może wtedy zapisać zakup i odpowiedzieć aplikacji, czy jest on prawidłowy.

Dzięki temu, że weryfikacja jest przeprowadzana przez usługę backendu w sklepach, a nie przez aplikację działającą na urządzeniu użytkownika, możesz zapobiec uzyskaniu przez użytkownika dostępu do funkcji premium, np. przez cofnięcie zegara systemowego.

Konfigurowanie po stronie Fluttera

Konfigurowanie uwierzytelniania

Ponieważ zakupy będą wysyłane do usługi backendu, musisz mieć pewność, że użytkownik jest uwierzytelniony podczas dokonywania zakupu. Większość logiki uwierzytelniania jest już dodana w projekcie początkowym. Musisz tylko sprawdzić, czy PurchasePage wyświetla przycisk logowania, gdy użytkownik nie jest jeszcze zalogowany. Dodaj ten kod na początku metody kompilacji pliku PurchasePage:

lib/pages/purchase_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

class PurchasePage extends StatelessWidget {
  const PurchasePage({super.key});

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

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

    // ...

Wywoływanie punktu końcowego weryfikacji połączeń z aplikacji

W aplikacji utwórz funkcję _verifyPurchase(PurchaseDetails purchaseDetails), która wywołuje punkt końcowy /verifypurchase na backendzie Dart za pomocą wywołania http post.

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

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

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart';                           // And this one

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

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

  final iapConnection = IAPConnection.instance;

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

Dodaj firebaseNotifier z utworzeniem DashPurchasesmain.dart:

lib/main.dart

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

Dodaj funkcję pobierającą użytkownika w klasie FirebaseNotifier, aby przekazywać identyfikator użytkownika do funkcji weryfikacji zakupu.

lib/logic/firebase_notifier.dart

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

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

lib/logic/dash_purchases.dart

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

Wywołaj funkcję _verifyPurchase_handlePurchase tuż przed zastosowaniem zakupu. Zastosuj zakup dopiero po jego zweryfikowaniu. W aplikacji produkcyjnej możesz to dodatkowo określić, np. zastosować subskrypcję próbną, gdy sklep jest tymczasowo niedostępny. W tym przykładzie zastosuj jednak zakup, gdy zostanie on zweryfikowany.

lib/logic/dash_purchases.dart

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

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

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
          case storeKeyConsumable:
            counter.addBoughtDashes(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

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

W aplikacji wszystko jest już gotowe do weryfikacji zakupów.

Konfigurowanie usługi backendu

Następnie skonfiguruj backend do weryfikowania zakupów.

Tworzenie modułów obsługi zakupu

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

be50c207c5a2a519.png

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

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
  /// Verify if non-subscription purchase (aka consumable) is valid
  /// and update the database
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });

  /// Verify if subscription purchase (aka non-consumable) is valid
  /// and update the database
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });
}

Jak widać, każda metoda wymaga 3 parametrów:

  • userId: Identyfikator zalogowanego użytkownika, dzięki czemu możesz powiązać zakupy z użytkownikiem.
  • productData: Dane o produkcie. Za chwilę to zdefiniujesz.
  • token: Token przekazany użytkownikowi przez sklep.

Aby ułatwić korzystanie z tych funkcji obsługi zakupu, dodaj metodę verifyPurchase(), której można używać zarówno w przypadku subskrypcji, jak i zakupów jednorazowych:

lib/purchase_handler.dart

  /// Verify if purchase is valid and update the database
  Future<bool> verifyPurchase({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    switch (productData.type) {
      case ProductType.subscription:
        return handleSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
      case ProductType.nonSubscription:
        return handleNonSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
    }
  }

Teraz w obu przypadkach możesz po prostu wywołać funkcję verifyPurchase, ale nadal mieć osobne implementacje.

Klasa ProductData zawiera podstawowe informacje o różnych produktach, które można kupić, w tym identyfikator produktu (czasami nazywany też SKU) i ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType może być subskrypcją lub nie.

lib/products.dart

enum ProductType { subscription, nonSubscription }

Na koniec lista produktów jest definiowana jako mapa w tym samym pliku.

lib/products.dart

const productDataMap = {
  'dash_consumable_2k': ProductData(
    'dash_consumable_2k',
    ProductType.nonSubscription,
  ),
  'dash_upgrade_3d': ProductData(
    'dash_upgrade_3d',
    ProductType.nonSubscription,
  ),
  'dash_subscription_doubler': ProductData(
    'dash_subscription_doubler',
    ProductType.subscription,
  ),
};

Następnie zdefiniuj implementacje zastępcze dla Sklepu Google Play i Apple App Store. Zacznij od Google Play:

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

lib/google_play_purchase_handler.dart

import 'dart:async';

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

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

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

  GooglePlayPurchaseHandler(this.androidPublisher, this.iapRepository);

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

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

Na razie zwraca true w przypadku metod obsługi. Wrócimy do nich później.

Jak być może zauważysz, konstruktor przyjmuje instancję klasy IapRepository. Obsługa zakupu używa tej instancji do późniejszego przechowywania informacji o zakupach w Firestore. Aby komunikować się z Google Play, używasz podanego AndroidPublisherApi.

Następnie wykonaj te same czynności w przypadku modułu obsługi sklepu z aplikacjami. Utwórz lib/app_store_purchase_handler.dart i ponownie dodaj klasę, która rozszerza PurchaseHandler:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

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

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(this.iapRepository);

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

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

Świetnie. Masz teraz 2 procedury obsługi zakupu. Następnie utwórz punkt końcowy interfejsu API weryfikacji zakupu.

Używanie modułów obsługi zakupu

Otwórz bin/server.dart i utwórz punkt końcowy API za pomocą shelf_route:

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

Future<void> main() async {
  final router = Router();

  final purchaseHandlers = await _createPurchaseHandlers();

  router.post('/verifypurchase', (Request request) async {
    final dynamic payload = json.decode(await request.readAsString());

    final (:userId, :source, :productData, :token) = getPurchaseData(payload);

    final result = await purchaseHandlers[source]!.verifyPurchase(
      userId: userId,
      productData: productData,
      token: token,
    );

    if (result) {
      return Response.ok('all good!');
    } else {
      return Response.internalServerError();
    }
  });

  await serveHandler(router.call);
}

({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
  if (payload case {
    'userId': String userId,
    'source': String source,
    'productId': String productId,
    'verificationData': String token,
  }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

Kod wykonuje te działania:

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

Po utworzeniu punktu końcowego interfejsu API musisz skonfigurować 2 procedury obsługi zakupu. Wymaga to wczytania kluczy konta usługi uzyskanych w poprzednim kroku i skonfigurowania dostępu do różnych usług, w tym interfejsu Android Publisher API i interfejsu Firebase Firestore API. Następnie utwórz 2 funkcje obsługi zakupu z różnymi zależnościami:

bin/server.dart

import 'dart:convert';
import 'dart:io'; // new

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
  // Configure Android Publisher API access
  final serviceAccountGooglePlay =
      File('assets/service-account-google-play.json').readAsStringSync();
  final clientCredentialsGooglePlay =
      auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
  final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
  ]);
  final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

  // Configure Firestore API access
  final serviceAccountFirebase =
      File('assets/service-account-firebase.json').readAsStringSync();
  final clientCredentialsFirebase =
      auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
  final clientFirebase =
      await auth.clientViaServiceAccount(clientCredentialsFirebase, [
    fs.FirestoreApi.cloudPlatformScope,
  ]);
  final firestoreApi = fs.FirestoreApi(clientFirebase);
  final dynamic json = jsonDecode(serviceAccountFirebase);
  final projectId = json['project_id'] as String;
  final iapRepository = IapRepository(firestoreApi, projectId);

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

Weryfikowanie zakupów na Androidzie: wdrażanie procedury obsługi zakupów

Następnie kontynuuj implementację modułu obsługi zakupu w Google Play.

Google udostępnia już pakiety Dart do interakcji z interfejsami API, których potrzebujesz do weryfikacji zakupów. Zostały one zainicjowane w pliku server.dart, a teraz są używane w klasie GooglePlayPurchaseHandler.

Zaimplementuj moduł obsługi zakupów, które nie są subskrypcjami:

lib/google_play_purchase_handler.dart

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order ID exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

W podobny sposób możesz zaktualizować moduł obsługi zakupu subskrypcji:

lib/google_play_purchase_handler.dart

  /// Handle subscription purchases.
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.subscriptions.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Subscription response: ${response.toJson()}');

      // Make sure an order ID exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = extractOrderId(response.orderId!);

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

Dodaj tę metodę, aby ułatwić analizowanie identyfikatorów zamówień, oraz 2 metody analizowania stanu zakupu.

lib/google_play_purchase_handler.dart

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

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

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

Zakupy w Google Play powinny zostać zweryfikowane i zapisane w bazie danych.

Następnie przejdź do zakupów w App Store na urządzeniach z iOS.

Weryfikowanie zakupów w iOS: wdrażanie modułu obsługi zakupów

Do weryfikacji zakupów w App Store służy pakiet Dart innej firmy o nazwie app_store_server_sdk, który ułatwia ten proces.

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

lib/app_store_purchase_handler.dart

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

W przeciwieństwie do interfejsów API Google Play App Store używa teraz tych samych punktów końcowych interfejsu API zarówno w przypadku subskrypcji, jak i produktów nieobjętych subskrypcją. Oznacza to, że możesz używać tej samej logiki w przypadku obu tych funkcji. Połącz je, aby wywoływały tę samą implementację:

lib/app_store_purchase_handler.dart

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

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

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {

    // See next step
  }

Teraz zaimplementuj handleValidation:

lib/app_store_purchase_handler.dart

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
    print('AppStorePurchaseHandler.handleValidation');
    final response = await _iTunesAPI.verifyReceipt(
      password: appStoreSharedSecret,
      receiptData: token,
    );
    print('response: $response');
    if (response.status == 0) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(
              NonSubscriptionPurchase(
                userId: userId,
                productId: receipt.productId ?? '',
                iapSource: IAPSource.appstore,
                orderId: receipt.originalTransactionId ?? '',
                purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0'),
                ),
                type: product.type,
                status: NonSubscriptionStatus.completed,
              ),
            );
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(
              SubscriptionPurchase(
                userId: userId,
                productId: receipt.productId ?? '',
                iapSource: IAPSource.appstore,
                orderId: receipt.originalTransactionId ?? '',
                purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0'),
                ),
                type: product.type,
                expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0'),
                ),
                status: SubscriptionStatus.active,
              ),
            );
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

Zakupy w App Store powinny być teraz zweryfikowane i przechowywane w bazie danych.

Uruchamianie backendu

W tym momencie możesz uruchomić polecenie dart bin/server.dart, aby udostępnić punkt końcowy /verifypurchase.

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

11. Monitorowanie zakupów

Zalecany sposób śledzenia zakupów użytkowników to usługa backendu. Dzieje się tak, ponieważ backend może odpowiadać na zdarzenia ze sklepu, a tym samym jest mniej podatny na nieaktualne informacje z powodu buforowania i mniej podatny na manipulacje.

Najpierw skonfiguruj przetwarzanie zdarzeń w sklepie na backendzie za pomocą backendu Dart, który tworzysz.

Przetwarzanie zdarzeń w sklepie na backendzie

Sklepy mogą informować Twój backend o wszelkich zdarzeniach związanych z płatnościami, np. o odnowieniu subskrypcji. Możesz przetwarzać te zdarzenia na backendzie, aby zakupy w bazie danych były aktualne. W tej sekcji skonfiguruj to ustawienie zarówno w Sklepie Google Play, jak i w Apple App Store.

Przetwarzanie zdarzeń rozliczeniowych w Google Play

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

Ponieważ jest to funkcja specyficzna dla Google Play, należy ją umieścić w sekcji GooglePlayPurchaseHandler.

Zacznij od otwarcia pliku lib/google_play_purchase_handler.dart i dodania importu PubsubApi:

lib/google_play_purchase_handler.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;

Następnie przekaż PubsubApi do GooglePlayPurchaseHandler i zmodyfikuj konstruktor klasy, aby utworzyć Timer w ten sposób:

lib/google_play_purchase_handler.dart

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;
  final pubsub.PubsubApi pubsubApi; // new

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
    this.pubsubApi, // new
  ) {
    // Poll messages from Pub/Sub every 10 seconds
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullMessageFromPubSub();
    });
  }

Usługa Timer jest skonfigurowana tak, aby wywoływać metodę _pullMessageFromPubSub co 10 sekund. Możesz dostosować czas trwania do własnych preferencji.

Następnie utwórz_pullMessageFromPubSub

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(maxMessages: 1000);
    final topicName =
        'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(ackIds: [id]);
    final subscriptionName =
        'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

Dodany właśnie kod komunikuje się z tematem Pub/Sub w Google Cloud co 10 sekund i prosi o nowe wiadomości. Następnie przetwarza każdą wiadomość metodą _processMessage.

Ta metoda dekoduje przychodzące wiadomości i uzyskuje zaktualizowane informacje o każdym zakupie, zarówno subskrypcji, jak i produktów nieobjętych subskrypcją, w razie potrzeby wywołując istniejącą funkcję handleSubscription lub handleNonSubscription.

Każda wiadomość musi zostać potwierdzona za pomocą metody _askMessage.

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

bin/server.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;      // Add this import

  final clientGooglePlay = await auth
      .clientViaServiceAccount(clientCredentialsGooglePlay, [
        ap.AndroidPublisherApi.androidpublisherScope,
        pubsub.PubsubApi.cloudPlatformScope,               // Add this line
      ]);

Następnie utwórz instancję PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Na koniec przekaż go do konstruktora GooglePlayPurchaseHandler:

bin/server.dart

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

Konfigurowanie Google Play

Masz już kod do wykorzystywania zdarzeń związanych z płatnościami z tematu Pub/Sub, ale nie masz jeszcze utworzonego tematu Pub/Sub ani nie publikujesz żadnych zdarzeń związanych z płatnościami. Czas to skonfigurować.

Najpierw utwórz temat Pub/Sub:

  1. Ustaw wartość googleCloudProjectIdconstants.dart na identyfikator Twojego projektu Google Cloud.
  2. Otwórz stronę Cloud Pub/Sub w konsoli Google Cloud.
  3. Upewnij się, że jesteś w projekcie Firebase, i kliknij + Utwórz temat. d5ebf6897a0a8bf5.png
  4. Nadaj nowemu tematowi nazwę identyczną z wartością ustawioną dla parametru googlePlayPubsubBillingTopic w pliku constants.dart. W takim przypadku nadaj mu nazwę play_billing. Jeśli wybierzesz coś innego, pamiętaj o zaktualizowaniu constants.dart. Utwórz temat. 20d690fc543c4212.png
  5. Na liście tematów Pub/Sub kliknij 3 pionowe kropki przy utworzonym przez Ciebie temacie i kliknij Wyświetl uprawnienia. ea03308190609fb.png
  6. Na pasku bocznym po prawej stronie kliknij Dodaj podmiot.
  7. Dodaj tutaj google-play-developer-notifications@system.gserviceaccount.com i przypisz mu rolę publikującego w Pub/Sub. 55631ec0549215bc.png
  8. Zapisz zmiany uprawnień.
  9. Skopiuj nazwę tematu, który właśnie został utworzony.
  10. Ponownie otwórz Konsolę Play i wybierz aplikację z listy Wszystkie aplikacje.
  11. Przewiń w dół i kliknij Zarabianie > Konfiguracja ustawień zarabiania.
  12. Wpisz pełny temat i zapisz zmiany. 7e5e875dc6ce5d54.png

Wszystkie zdarzenia związane z płatnościami w Google Play będą teraz publikowane w tym temacie.

Przetwarzanie zdarzeń związanych z płatnościami w App Store

Następnie wykonaj te same czynności w przypadku zdarzeń rozliczeniowych w App Store. Istnieją 2 skuteczne sposoby implementowania obsługi aktualizacji w przypadku zakupów w App Store. Jednym z nich jest wdrożenie elementu webhook, który udostępniasz Apple i którego firma używa do komunikacji z Twoim serwerem. Drugi sposób, który znajdziesz w tych ćwiczeniach z programowania, polega na połączeniu się z interfejsem App Store Server API i ręcznym uzyskaniu informacji o subskrypcji.

Ten codelab skupia się na drugim rozwiązaniu, ponieważ w przypadku webhooka musisz udostępnić serwer w internecie.

W środowisku produkcyjnym najlepiej mieć oba te elementy. Webhook do pobierania zdarzeń z App Store oraz interfejs Server API, jeśli przegapisz zdarzenie lub musisz ponownie sprawdzić stan subskrypcji.

Zacznij od otwarcia pliku lib/app_store_purchase_handler.dart i dodania zależności AppStoreServerAPI:

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,                                  // And this parameter
  );

Zmodyfikuj konstruktor, aby dodać licznik czasu, który będzie wywoływać metodę _pullStatus. Ten licznik będzie wywoływać metodę _pullStatus co 10 sekund. Możesz dostosować czas trwania tego timera do swoich potrzeb.

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

Następnie utwórz metodę _pullStatus w ten sposób:

lib/app_store_purchase_handler.dart

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where(
      (element) =>
          element.type == ProductType.subscription &&
          element.iapSource == IAPSource.appstore,
    );
    for (final purchase in appStoreSubscriptions) {
      final status = await appStoreServerAPI.getAllSubscriptionStatuses(
        purchase.orderId,
      );
      // Obtain all subscriptions for the order ID.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
            transaction.transactionInfo.expiresDate ?? 0,
          );
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(
            SubscriptionPurchase(
              userId: null,
              productId: transaction.transactionInfo.productId,
              iapSource: IAPSource.appstore,
              orderId: transaction.originalTransactionId,
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate,
              ),
              type: ProductType.subscription,
              expiryDate: expirationDate,
              status: isExpired
                  ? SubscriptionStatus.expired
                  : SubscriptionStatus.active,
            ),
          );
        }
      }
    }
  }

Ta metoda działa w ten sposób:

  1. Pobiera listę aktywnych subskrypcji z Firestore za pomocą IapRepository.
  2. W przypadku każdego zamówienia wysyła żądanie stanu subskrypcji do interfejsu App Store Server API.
  3. Pobiera ostatnią transakcję zakupu subskrypcji.
  4. sprawdza datę 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

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


  // add from here
  final subscriptionKeyAppStore = File(
    'assets/SubscriptionKey.p8',
  ).readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
      appStoreServerAPI,                                     // Add this argument
    ),
  };

Konfiguracja App Store

Następnie skonfiguruj App Store:

  1. Zaloguj się w App Store Connect i wybierz Użytkownicy i dostęp.
  2. Kliknij Integrations > Keys > In-App Purchase (Integracje > Klucze > Zakup w aplikacji).
  3. Aby dodać nowy, kliknij ikonę „+”.
  4. Nadaj mu nazwę, np. „Klucz Codelab”.
  5. Pobierz plik p8 zawierający klucz.
  6. Skopiuj go do folderu zasobów pod 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 wydawcy z góry listy kluczy i ustaw go jako stałą 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ż zabezpieczenie klienta jest trudne. Musisz jednak mieć możliwość przekazywania informacji z powrotem do klienta, aby aplikacja mogła reagować na informacje o stanie subskrypcji. Przechowując zakupy w Firestore, możesz synchronizować dane z klientem i automatycznie je aktualizować.

W aplikacji jest już uwzględniony element IAPRepo, czyli repozytorium Firestore, które zawiera wszystkie dane o zakupach użytkownika w List<PastPurchase> purchases. W repozytorium znajduje się też hasActiveSubscription,, co oznacza, że zakup productId storeKeySubscription ma stan, który nie wygasł. Gdy użytkownik nie jest zalogowany, lista jest pusta.

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

      hasActiveSubscription = purchases.any(
        (element) =>
            element.productId == storeKeySubscription &&
            element.status != Status.expired,
      );

      hasUpgrade = purchases.any(
        (element) => element.productId == storeKeyUpgrade,
      );

      notifyListeners();
    });
  }

Cała logika zakupu znajduje się w klasie DashPurchases. To w niej należy stosować lub usuwać subskrypcje. Dodaj więc iapRepo jako właściwość w klasie i przypisz iapRepo w konstruktorze. Następnie dodaj bezpośrednio słuchacza w konstruktorze i usuń go w metodzie dispose(). Początkowo detektor może być pustą funkcją. Ponieważ IAPRepo jest ChangeNotifier, a funkcja notifyListeners() jest wywoływana za każdym razem, gdy zmieniają się zakupy w Firestore, metoda purchasesUpdate() jest zawsze wywoływana, gdy zmieniają się zakupione produkty.

lib/logic/dash_purchases.dart

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

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

  Future<void> loadPurchases() async {
    // Elided.
  }

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

  void purchasesUpdate() {
    //TODO manage updates
  }

Następnie przekaż IAPRepo do konstruktora w main.dart.. Repozytorium możesz uzyskać za pomocą context.read, ponieważ zostało już utworzone w Provider.

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),                         // Add this line
          ),
          lazy: false,
        ),

Następnie napisz kod funkcji purchaseUpdate(). W metodach dash_counter.dart,applyPaidMultiplier ustaw odpowiednio mnożnik na 10 lub 1, aby nie trzeba było sprawdzać, czy subskrypcja została już zastosowana.removePaidMultiplier Gdy stan subskrypcji się zmieni, zaktualizuj też stan produktu, który można kupić, aby na stronie zakupu wyświetlać informację, że jest on już aktywny. Ustaw właściwość _beautifiedDashUpgrade w zależności od tego, czy uaktualnienie zostało kupione.

lib/logic/dash_purchases.dart

  void purchasesUpdate() {
    var subscriptions = <PurchasableProduct>[];
    var upgrades = <PurchasableProduct>[];
    // Get a list of purchasable products for the subscription and upgrade.
    // This should be 1 per type.
    if (products.isNotEmpty) {
      subscriptions = products
          .where((element) => element.productDetails.id == storeKeySubscription)
          .toList();
      upgrades = products
          .where((element) => element.productDetails.id == storeKeyUpgrade)
          .toList();
    }

    // Set the subscription in the counter logic and show/hide purchased on the
    // purchases page.
    if (iapRepo.hasActiveSubscription) {
      counter.applyPaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchased);
      }
    } else {
      counter.removePaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchasable);
      }
    }

    // Set the Dash beautifier and show/hide purchased on
    // the purchases page.
    if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
      _beautifiedDashUpgrade = iapRepo.hasUpgrade;
      for (var element in upgrades) {
        _updateStatus(
          element,
          _beautifiedDashUpgrade
              ? ProductStatus.purchased
              : ProductStatus.purchasable,
        );
      }
      notifyListeners();
    }
  }

  void _updateStatus(PurchasableProduct product, ProductStatus status) {
    if (product.status != ProductStatus.purchased) {
      product.status = ProductStatus.purchased;
      notifyListeners();
    }
  }

Dzięki temu stan subskrypcji i ulepszenia będzie zawsze aktualny w usłudze backendu i zsynchronizowany z aplikacją. Aplikacja będzie odpowiednio reagować i stosować funkcje subskrypcji i ulepszenia w grze Dash clicker.

12. Wszystko gotowe

Gratulacje!!! To już koniec tego laboratorium. Gotowy kod do tego ćwiczenia znajdziesz w android_studio_folder.pngfolderze complete.

Aby dowiedzieć się więcej, wypróbuj inne codelaby Fluttera.