Добавление встроенных покупок в ваше приложение Flutter

1. Введение

Последнее обновление: 11 июля 2023 г.

Для добавления покупок из приложения в приложение Flutter требуется правильная настройка магазинов App и Play, проверка покупки и предоставление необходимых разрешений, таких как льготы по подписке.

В этой лаборатории кода вы добавите в приложение (предоставленное вам) три типа покупок внутри приложения и подтвердите эти покупки с помощью серверной части Dart с Firebase. Прилагаемое приложение Dash Clicker содержит игру, в которой в качестве валюты используется талисман Dash. Вы добавите следующие варианты покупки:

  1. Возможность повторной покупки сразу за 2000 Dash.
  2. Единовременная покупка обновления, позволяющая превратить старый стиль Dash в современный Dash.
  3. Подписка, которая удваивает автоматически генерируемые клики.

Первый вариант покупки дает пользователю прямую выгоду в размере 2000 Dash. Они доступны непосредственно пользователю и могут быть куплены много раз. Это называется расходным материалом, поскольку он потребляется напрямую и может потребляться несколько раз.

Второй вариант делает Dash более красивым. Его нужно купить только один раз, и он доступен навсегда. Такая покупка называется непотребляемой, поскольку она не может быть использована приложением, но действительна навсегда.

Третий и последний вариант покупки — подписка. Пока подписка активна, пользователь будет получать Dash быстрее, но когда он перестанет платить за подписку, преимущества также пропадут.

Серверная служба (также предоставляемая вам) работает как приложение Dart, проверяет совершение покупок и сохраняет их с помощью Firestore. Firestore используется для упрощения процесса, но в рабочем приложении вы можете использовать любой тип серверной службы.

300123416ebc8dc1.png7145d0fffe6ea741.png646317a79be08214.png

Что ты построишь

  • Вы расширите приложение для поддержки покупок расходных материалов и подписок.
  • Вы также расширите серверное приложение Dart для проверки и хранения приобретенных товаров.

Что вы узнаете

  • Как настроить App Store и Play Store для приобретаемых продуктов.
  • Как общаться с магазинами, чтобы проверять покупки и сохранять их в Firestore.
  • Как управлять покупками в вашем приложении.

Что вам понадобится

  • Android Studio 4.1 или новее
  • Xcode 12 или новее (для разработки под iOS)
  • Флаттер SDK

2. Настройте среду разработки

Чтобы запустить эту лабораторию кода, загрузите код и измените идентификатор пакета для iOS и имя пакета для Android.

Загрузите код

Чтобы клонировать репозиторий GitHub из командной строки, используйте следующую команду:

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

Или, если у вас установлен инструмент командной строки GitHub , используйте следующую команду:

gh repo clone flutter/codelabs flutter-codelabs

Пример кода клонируется в каталог flutter-codelabs , содержащий код для коллекции codelabs. Код этой лаборатории кода находится в flutter-codelabs/in_app_purchases .

Структура каталогов flutter-codelabs/in_app_purchases содержит серию снимков того, где вы должны находиться в конце каждого именованного шага. Стартовый код находится на шаге 0, поэтому найти соответствующие файлы так же просто, как:

cd flutter-codelabs/in_app_purchases/step_00

Если вы хотите пропустить вперед или посмотреть, как что-то должно выглядеть после определенного шага, загляните в каталог, названный в честь интересующего вас шага. Код последнего шага находится в папке complete .

Настройте стартовый проект

Откройте стартовый проект с step_00 в вашей любимой IDE. Для снимков экрана мы использовали Android Studio, но Visual Studio Code также является отличным вариантом. В любом из редакторов убедитесь, что установлены последние версии плагинов Dart и Flutter.

Приложения, которые вы собираетесь создавать, должны взаимодействовать с App Store и Play Store, чтобы знать, какие продукты доступны и по какой цене. Каждое приложение идентифицируется уникальным идентификатором. Для iOS App Store это называется идентификатором пакета, а для Android Play Store — это идентификатор приложения. Эти идентификаторы обычно создаются с использованием обратной записи имени домена. Например, при создании приложения для покупок в приложении для flutter.dev мы будем использовать dev.flutter.inapppurchase . Придумайте идентификатор для вашего приложения, теперь вы собираетесь установить его в настройках проекта.

Сначала настройте идентификатор пакета для iOS.

Открыв проект в Android Studio, щелкните правой кнопкой мыши папку iOS, выберите Flutter и откройте модуль в приложении Xcode.

942772eb9a73bfaa.png

В структуре папок Xcode проект Runner находится вверху, а целевые объекты Flutter , Runner и Products находятся под проектом Runner. Дважды щелкните Runner, чтобы изменить настройки проекта, и нажмите «Подписание и возможности» . Введите идентификатор пакета, который вы только что выбрали, в поле «Команда» , чтобы указать свою команду.

812f919d965c649a.jpeg

Теперь вы можете закрыть Xcode и вернуться в Android Studio, чтобы завершить настройку Android. Для этого откройте файл build.gradle в разделе android/app, и измените свой applicationId (строка 37 на снимке экрана ниже) на идентификатор приложения, такой же, как идентификатор пакета iOS. Обратите внимание, что идентификаторы магазинов iOS и Android не обязательно должны быть идентичными, однако сохранение их идентичности снижает вероятность ошибок, поэтому в этой лаборатории кода мы также будем использовать идентичные идентификаторы.

5c4733ac560ae8c2.png

3. Установите плагин

В этой части лабораторной работы вы установите плагин in_app_purchase.

Добавить зависимость в pubspec

Добавьте in_app_purchase в pubspec, добавив in_app_purchase к зависимостям в вашей pubspec:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

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

Нажмите pub get , чтобы загрузить пакет, или запустите flutter pub get в командной строке.

4. Настройте магазин приложений.

Чтобы настроить встроенные покупки и протестировать их на iOS, вам необходимо создать новое приложение в App Store и создавать там покупаемые продукты. Вам не нужно ничего публиковать или отправлять приложение на проверку в Apple. Для этого вам понадобится учетная запись разработчика. Если у вас его нет, зарегистрируйтесь в программе разработчиков Apple .

Чтобы использовать покупки в приложении, вам также необходимо иметь активное соглашение для платных приложений в App Store Connect. Перейдите на https://appstoreconnect.apple.com/ и нажмите «Соглашения, налоги и банковские операции» .

6e373780e5e24a6f.png

Здесь вы увидите соглашения для бесплатных и платных приложений. Статус бесплатных приложений должен быть активным, а статус платных — новым. Обязательно ознакомьтесь с условиями, примите их и введите всю необходимую информацию.

74c73197472c9aec.png

Если все настроено правильно, статус платных приложений будет активен. Это очень важно, поскольку вы не сможете опробовать покупки в приложении без активного соглашения.

4a100bbb8cafdbbf.jpeg

Зарегистрировать идентификатор приложения

Создайте новый идентификатор на портале разработчиков Apple.

55d7e592d9a3fc7b.png

Выберите идентификаторы приложений

13f125598b72ca77.png

Выберите приложение

41ac4c13404e2526.png

Предоставьте некоторое описание и установите идентификатор пакета, чтобы он соответствовал тому же значению, которое было ранее установлено в XCode.

9d2c940ad80deef.png

Дополнительные инструкции о том, как создать новый идентификатор приложения, см. в справке по учетной записи разработчика .

Создание нового приложения

Создайте новое приложение в App Store Connect, используя свой уникальный идентификатор пакета.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Дополнительные инструкции о том, как создать новое приложение и управлять соглашениями, см. в справке App Store Connect .

Чтобы протестировать покупки в приложении, вам понадобится тестовый пользователь песочницы. Этот тестовый пользователь не должен быть подключен к iTunes — он используется только для тестирования покупок в приложении. Вы не можете использовать адрес электронной почты, который уже используется для учетной записи Apple. В разделе «Пользователи и доступ» перейдите в раздел «Тестеры» в разделе «Песочница» , чтобы создать новую учетную запись «песочницы» или управлять существующими идентификаторами Apple ID «песочницы».

3ca2b26d4e391a4c.jpeg

Теперь вы можете настроить пользователя песочницы на своем iPhone, выбрав «Настройки» > «App Store» > «Учетная запись песочницы».

c7dadad2c1d448fa.jpeg5363f87efcddaa4.jpeg

Настройка покупок в приложении

Теперь вы настроите три покупаемых предмета:

  • dash_consumable_2k : покупка расходных материалов, которую можно приобретать много раз, которая дает пользователю 2000 тире (валюта в приложении) за каждую покупку.
  • dash_upgrade_3d : нерасходуемая покупка «обновления», которую можно приобрести только один раз, и которая дает пользователю косметически другой тир, который можно щелкнуть.
  • dash_subscription_doubler : подписка, которая предоставляет пользователю вдвое больше Dash за клик в течение всего срока действия подписки.

d156b2f5bac43ca8.png

Откройте «Покупки в приложении» > «Управление» .

Создайте свои покупки в приложении с указанными идентификаторами:

  1. Настройте dash_consumable_2k в качестве расходного материала .

Используйте dash_consumable_2k в качестве идентификатора продукта. Ссылочное имя используется только при подключении к магазину приложений, просто установите для него dash consumable 2k и добавьте свои локализации для покупки. Позвоните о покупке Spring is in the air , из описания 2000 dashes fly out .

ec1701834fd8527.png

  1. Настройте dash_upgrade_3d как нерасходуемый файл .

Используйте dash_upgrade_3d в качестве идентификатора продукта. Установите ссылочное имя dash upgrade 3d и добавьте свои локализации для покупки. Позвоните о покупке 3D Dash с Brings your dash back to the future как описано в описании.

6765d4b711764c30.png

  1. Настройте dash_subscription_doubler как подписку с автоматическим продлением .

Схема подписки немного другая. Сначала вам нужно будет установить справочное имя и идентификатор продукта:

6d29e08dae26a0c4.png

Далее вам необходимо создать группу подписки. Если несколько подписок входят в одну группу, пользователь может подписаться только на одну из них одновременно, но может легко повысить или понизить версию одной из этих подписок. Просто позвоните в эту группу subscriptions .

5bd0da17a85ac076.png

Далее введите продолжительность подписки и локализации. Назовите эту подписку Jet Engine с описанием. Doubles your clicks . Нажмите Сохранить .

bd1b1d82eeee4cb3.png

После того, как вы нажали кнопку «Сохранить» , добавьте стоимость подписки. Выбирайте любую цену по вашему желанию.

d0bf39680ef0aa2e.png

Теперь вы должны увидеть три покупки в списке покупок:

99d5c4b446e8fecf.png

5. Настройте Play Маркет.

Как и в случае с App Store, вам также понадобится учетная запись разработчика для Play Store. Если у вас его еще нет, зарегистрируйте аккаунт .

Создать новое приложение

Создайте новое приложение в консоли Google Play:

  1. Откройте консоль Play .
  2. Выберите Все приложения > Создать приложение.
  3. Выберите язык по умолчанию и добавьте название для своего приложения. Введите название своего приложения так, как вы хотите, чтобы оно отображалось в Google Play. Вы можете изменить имя позже.
  4. Укажите, что ваше приложение является игрой. Вы можете изменить это позже.
  5. Укажите, является ли ваше приложение бесплатным или платным.
  6. Добавьте адрес электронной почты, который пользователи Play Store смогут использовать, чтобы связаться с вами по поводу этого приложения.
  7. Заполните правила содержания и декларации законов США об экспорте.
  8. Выберите Создать приложение .

После создания приложения перейдите на панель управления и выполните все задачи в разделе «Настройка приложения» . Здесь вы предоставляете некоторую информацию о своем приложении, например рейтинги контента и снимки экрана. 13845badcf9bc1db.png

Подпишите заявку

Чтобы иметь возможность протестировать покупки в приложении, вам необходимо загрузить хотя бы одну сборку в Google Play.

Для этого вам нужно, чтобы ваша сборка выпуска была подписана чем-то другим, кроме ключей отладки.

Создать хранилище ключей

Если у вас уже есть хранилище ключей, перейдите к следующему шагу. Если нет, создайте его, выполнив следующую команду в командной строке.

На Mac/Linux используйте следующую команду:

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

В Windows используйте следующую команду:

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

Эта команда сохраняет файл key.jks в вашем домашнем каталоге. Если вы хотите сохранить файл в другом месте, измените аргумент, который вы передаете параметру -keystore . Держите

keystore

файл частный; не проверяйте это в общедоступной системе контроля версий!

Ссылка на хранилище ключей из приложения

Создайте файл с именем <your app dir>/android/key.properties , содержащий ссылку на ваше хранилище ключей:

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>

Настроить подпись в Gradle

Настройте подпись для своего приложения, отредактировав файл <your app dir>/android/app/build.gradle .

Добавьте информацию о хранилище ключей из файла свойств перед блоком android :

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         // omitted
   }

Загрузите файл key.properties в объект keystoreProperties .

Добавьте следующий код перед блоком buildTypes :

   buildTypes {
       release {
           // TODO: Add your own signing config for the release build.
           // Signing with the debug keys for now,
           // so `flutter run --release` works.
           signingConfig signingConfigs.debug
       }
   }

Настройте блок signingConfigs в файле build.gradle вашего модуля, указав информацию о конфигурации подписи:

   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

Релизные сборки вашего приложения теперь будут подписываться автоматически.

Дополнительную информацию о подписании приложения см. в разделе «Подписание приложения» на сайте Developer.android.com .

Загрузите свою первую сборку

После того, как ваше приложение настроено для подписи, вы сможете создать свое приложение, запустив:

flutter build appbundle

Эта команда по умолчанию генерирует сборку выпуска, а выходные данные можно найти в <your app dir>/build/app/outputs/bundle/release/

На панели инструментов консоли Google Play выберите «Выпуск» > «Тестирование» > «Закрытое тестирование» и создайте новый выпуск для закрытого тестирования.

В этой лаборатории кода вы будете придерживаться подписания приложения Google, поэтому нажмите «Продолжить» в разделе «Подписание приложений в Play», чтобы согласиться.

ba98446d9c5c40e0.png

Затем загрузите пакет приложения app-release.aab , созданный командой сборки.

Нажмите «Сохранить» , а затем нажмите «Просмотреть выпуск».

Наконец, нажмите «Начать развертывание внутреннего тестирования» , чтобы активировать версию для внутреннего тестирования.

Настройка тестовых пользователей

Чтобы иметь возможность тестировать покупки в приложении, учетные записи Google ваших тестировщиков должны быть добавлены в консоль Google Play в двух местах:

  1. К конкретному тестовому треку (Внутреннее тестирование)
  2. В качестве тестера лицензий

Сначала начните с добавления тестера в трек внутреннего тестирования. Вернитесь в раздел «Релиз» > «Тестирование» > «Внутреннее тестирование» и перейдите на вкладку «Тестеры» .

a0d0394e85128f84.png

Создайте новый список адресов электронной почты, нажав Создать список адресов электронной почты . Дайте списку имя и добавьте адреса электронной почты учетных записей Google, которым необходим доступ для тестирования покупок в приложении.

Далее установите флажок рядом со списком и нажмите Сохранить изменения .

Затем добавьте тестеры лицензий:

  1. Вернитесь к представлению «Все приложения» консоли Google Play.
  2. Откройте «Настройки» > «Проверка лицензии» .
  3. Добавьте те же адреса электронной почты тестировщиков, которые должны иметь возможность тестировать покупки в приложении.
  4. Установите ответ лицензии на RESPOND_NORMALLY .
  5. Нажмите Сохранить изменения.

a1a0f9d3e55ea8da.png

Настройка покупок в приложении

Теперь вы настроите предметы, которые можно приобрести в приложении.

Как и в App Store, вам необходимо определить три разные покупки:

  • dash_consumable_2k : покупка расходных материалов, которую можно приобретать много раз, которая дает пользователю 2000 тире (валюта в приложении) за каждую покупку.
  • dash_upgrade_3d : нерасходуемая покупка «обновления», которую можно приобрести только один раз, что дает пользователю косметически другой тир, который можно щелкнуть.
  • dash_subscription_doubler : подписка, которая предоставляет пользователю вдвое больше Dash за клик в течение всего срока действия подписки.

Во-первых, добавьте расходные и нерасходные материалы.

  1. Перейдите в консоль Google Play и выберите свое приложение.
  2. Откройте «Монетизация» > «Продукты» > «Продукты для продажи в приложении» .
  3. Нажмите Создать продукт. c8d66e32f57dee21.png
  4. Введите всю необходимую информацию о вашем продукте. Убедитесь, что идентификатор продукта точно соответствует идентификатору, который вы собираетесь использовать.
  5. Нажмите «Сохранить».
  6. Нажмите «Активировать» .
  7. Повторите процедуру для покупки нерасходуемого «обновления».

Далее добавляем подписку:

  1. Перейдите в консоль Google Play и выберите свое приложение.
  2. Откройте «Монетизация» > «Продукты» > «Подписки» .
  3. Нажмите Создать подписку. 32a6a9eefdb71dd0.png
  4. Введите всю необходимую информацию для вашей подписки. Убедитесь, что идентификатор продукта точно соответствует идентификатору, который вы собираетесь использовать.
  5. Нажмите « Сохранить».

Теперь ваши покупки должны быть настроены в Play Console.

6. Настройте Firebase

В этой лаборатории кода вы будете использовать серверную службу для проверки и отслеживания покупок пользователей.

Использование серверной службы имеет ряд преимуществ:

  • Вы можете безопасно проверять транзакции.
  • Вы можете реагировать на события выставления счетов из магазинов приложений.
  • Вы можете отслеживать покупки в базе данных.
  • Пользователи не смогут обманом заставить ваше приложение предоставить дополнительные функции, переведя системные часы назад.

Хотя существует множество способов настроить серверную службу, вы сделаете это с помощью облачных функций и Firestore, используя собственную Firebase от Google.

Написание серверной части выходит за рамки этой лаборатории, поэтому стартовый код уже включает проект Firebase, который обрабатывает базовые покупки, чтобы вы могли начать работу.

Плагины Firebase также включены в стартовое приложение.

Вам осталось создать собственный проект Firebase, настроить приложение и серверную часть для Firebase и, наконец, развернуть серверную часть.

Создать проект Firebase

Перейдите в консоль Firebase и создайте новый проект Firebase. В этом примере вызовите проект Dash Clicker.

В backend-приложении вы привязываете покупки к конкретному пользователю, поэтому вам нужна аутентификация. Для этого используйте модуль аутентификации Firebase со входом в Google.

  1. На панели управления Firebase перейдите в раздел «Аутентификация» и включите ее, если необходимо.
  2. Перейдите на вкладку «Метод входа» и включите поставщика входа в систему Google .

7babb48832fbef29.png

Поскольку вы также будете использовать базу данных Firestore Firebase, включите и ее.

e20553e0de5ac331.png

Установите правила Cloud Firestore следующим образом:

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

Настройте Firebase для Flutter

Рекомендуемый способ установки Firebase в приложении Flutter — использовать интерфейс командной строки FlutterFire. Следуйте инструкциям, описанным на странице настройки .

При запуске flutterfire configure выберите проект, который вы только что создали на предыдущем шаге.

$ 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>  

Затем включите iOS и Android , выбрав две платформы.

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

Когда будет предложено переопределить firebase_options.dart, выберите «Да».

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

Настройка Firebase для Android: дальнейшие шаги

На панели управления Firebase перейдите в раздел «Обзор проекта», выберите «Настройки» и выберите вкладку «Общие» .

Прокрутите вниз до раздела «Ваши приложения» и выберите приложение DashClicker (Android) .

b22d46a759c0c834.png

Чтобы разрешить вход в Google в режиме отладки, вам необходимо предоставить хэш-отпечаток SHA-1 вашего сертификата отладки.

Получите хеш сертификата подписи отладки

В корне вашего проекта приложения Flutter измените каталог на папку android/ , а затем создайте отчет о подписи.

cd android
./gradlew :app:signingReport

Вам будет представлен большой список ключей подписи. Поскольку вам нужен хэш сертификата отладки, найдите сертификат со свойствами Variant и Config , для которых установлено debug . Скорее всего, хранилище ключей будет находиться в вашей домашней папке под .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

Скопируйте хеш SHA-1 и заполните последнее поле в модальном диалоговом окне отправки приложения.

Настройка Firebase для iOS: дальнейшие шаги

Откройте рабочее пространство ios/Runnder.xcworkspace с помощью Xcode . Или с помощью вашей IDE по выбору.

В VSCode щелкните правой кнопкой мыши папку ios/ и затем open in xcode .

В Android Studio щелкните правой кнопкой мыши папку ios/ , затем щелкните flutter а затем open iOS module in Xcode .

Чтобы разрешить вход в Google на iOS, добавьте параметр конфигурации CFBundleURLTypes в файлы plist сборки. (Дополнительную информацию можно найти в документации пакета google_sign_in .) В данном случае это файлы ios/Runner/Info-Debug.plist и ios/Runner/Info-Release.plist .

Пара ключ-значение уже добавлена, но их значения необходимо заменить:

  1. Получите значение REVERSED_CLIENT_ID из файла GoogleService-Info.plist без окружающего его элемента <string>..</string> .
  2. Замените значение в файлах ios/Runner/Info-Debug.plist и ios/Runner/Info-Release.plist под ключом 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>

Теперь вы закончили настройку Firebase.

7. Слушайте обновления о покупках

В этой части лабораторной работы вы подготовите приложение для покупки продуктов. Этот процесс включает в себя прослушивание обновлений и ошибок покупки после запуска приложения.

Слушайте обновления о покупках

В main.dart, найдите виджет MyHomePage , который имеет Scaffold с BottomNavigationBar , содержащим две страницы. На этой странице также создаются три Provider для DashCounter , DashUpgrades, и DashPurchases . DashCounter отслеживает текущее количество тире и автоматически увеличивает его. DashUpgrades управляет обновлениями, которые вы можете купить за Dashes. Эта кодовая лаборатория посвящена DashPurchases .

По умолчанию объект провайдера определяется при первом запросе этого объекта. Этот объект прослушивает обновления покупки непосредственно при запуске приложения, поэтому отключите отложенную загрузку этого объекта с помощью lazy: false :

библиотека/main.dart

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

Вам также понадобится экземпляр InAppPurchaseConnection . Однако, чтобы приложение оставалось тестируемым, вам нужен какой-то способ имитировать соединение. Для этого создайте метод экземпляра, который можно будет переопределить в тесте, и добавьте его в main.dart .

библиотека/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!;
  }
}

Вам необходимо немного обновить тест, если вы хотите, чтобы тест продолжал работать. Полный код TestIAPConnection можно найти в widget_test.dart на GitHub.

тест/widget_test.dart

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

В lib/logic/dash_purchases.dart перейдите к коду DashPurchases ChangeNotifier . В настоящее время к купленным Dash можно добавить только DashCounter .

Добавьте свойство подписки на поток, _subscription (типа StreamSubscription<List<PurchaseDetails>> _subscription; ), IAPConnection.instance, и импорт. Результирующий код должен выглядеть следующим образом:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

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

  DashPurchases(this.counter);
}

Ключевое слово late добавляется в _subscription поскольку _subscription инициализируется в конструкторе. По умолчанию этот проект настроен как не допускающий значения NULL (NNBD). Это означает, что свойства, которые не объявлены допускающими значение NULL, должны иметь значение, отличное от NULL. Квалификатор late позволяет отложить определение этого значения.

В конструкторе получите purchaseUpdatedStream и начните прослушивать поток. В методе dispose() отмените подписку на поток.

lib/logic/dash_purchases.dart

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

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

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

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

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

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

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

Теперь приложение получает обновления о покупках, поэтому в следующем разделе вы совершите покупку!

Прежде чем продолжить, запустите тесты с помощью « flutter test" чтобы убедиться, что все настроено правильно.

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. Совершайте покупки

В этой части лабораторной работы вы замените существующие в настоящее время макеты реальными покупаемыми продуктами. Эти продукты загружаются из магазинов, показаны в списке, и приобретаются при нажатии на продукт.

Адаптировать покупаемый продукт

PurchasableProduct отображает макет продукта. Обновите его, чтобы отображать актуальное содержимое, заменив класс PurchasableProduct в purchasable_product.dart следующим кодом:

lib/модель/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, удалите фиктивные покупки и замените их пустым списком List<PurchasableProduct> products = [];

Загрузить доступные покупки

Чтобы дать пользователю возможность совершить покупку, загрузите покупки из магазина. Сначала проверьте, доступен ли магазин. Если хранилище недоступно, установка storeState значения notAvailable отображает пользователю сообщение об ошибке.

lib/logic/dash_purchases.dart

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

Когда магазин будет доступен, загрузите доступные покупки. Учитывая предыдущую настройку Firebase, ожидайте увидеть storeKeyConsumable , storeKeySubscription, и storeKeyUpgrade . Если ожидаемая покупка недоступна, выведите эту информацию на консоль; вы также можете отправить эту информацию во внутреннюю службу.

Метод await iapConnection.queryProductDetails(ids) возвращает как не найденные идентификаторы, так и найденные доступные для покупки продукты. Используйте productDetails из ответа, чтобы обновить пользовательский интерфейс, и установите для StoreState значение available .

lib/logic/dash_purchases.dart

import '../constants.dart';

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

Вызовите функцию loadPurchases() в конструкторе:

lib/logic/dash_purchases.dart

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

Наконец, измените значение поля storeState со StoreState.available на StoreState.loading:

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Покажите покупаемые продукты

Рассмотрим файл purchase_page.dart . Виджет PurchasePage отображает _PurchasesLoading , _PurchaseList, или _PurchasesNotAvailable, в зависимости от StoreState . Виджет также показывает прошлые покупки пользователя, которые используются на следующем шаге.

Виджет _PurchaseList показывает список доступных для покупки продуктов и отправляет запрос на покупку объекту 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(),
    );
  }
}

Вы сможете увидеть доступные продукты в магазинах Android и iOS, если они настроены правильно. Обратите внимание, что может пройти некоторое время, прежде чем покупки станут доступны при вводе в соответствующие консоли.

ca1a9f97c21e552d.png

Вернитесь к dash_purchases.dart и реализуйте функцию покупки продукта. Вам нужно только отделить расходные материалы от нерасходных. Продукты обновления и подписки не являются расходными материалами.

lib/logic/dash_purchases.dart

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

Прежде чем продолжить, создайте переменную _beautifiedDashUpgrade и обновите геттер beautifiedDash , чтобы он ссылался на нее.

lib/logic/dash_purchases.dart

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

Метод _onPurchaseUpdate получает обновления о покупках, обновляет статус продукта, отображаемый на странице покупки, и применяет покупку к логике счетчика. Важно вызвать completePurchase после обработки покупки, чтобы магазин знал, что покупка обработана правильно.

lib/logic/dash_purchases.dart

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

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

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

9. Настройте серверную часть

Прежде чем переходить к отслеживанию и проверке покупок, настройте серверную часть Dart для поддержки этого.

В этом разделе работайте с корневой папкой dart-backend/ .

Убедитесь, что у вас установлены следующие инструменты:

Обзор базового проекта

Поскольку некоторые части этого проекта считаются выходящими за рамки данной лаборатории, они включены в стартовый код. Прежде чем приступить к работе, рекомендуется просмотреть то, что уже есть в стартовом коде, чтобы получить представление о том, как вы собираетесь его структурировать.

Этот серверный код может работать локально на вашем компьютере, вам не нужно его развертывать, чтобы использовать. Однако вам необходимо иметь возможность подключения вашего устройства разработки (Android или iPhone) к компьютеру, на котором будет работать сервер. Для этого они должны находиться в одной сети, и вам необходимо знать IP-адрес вашего компьютера.

Попробуйте запустить сервер с помощью следующей команды:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Серверная часть Dart использует shelf и shelf_router для обслуживания конечных точек API. По умолчанию сервер не предоставляет никаких маршрутов. Позже вы создадите маршрут для обработки процесса проверки покупки.

Одна часть, которая уже включена в стартовый код, — это IapRepository в lib/iap_repository.dart . Поскольку изучение взаимодействия с Firestore или базами данных в целом не считается актуальным для этой лаборатории кода, стартовый код содержит функции для создания или обновления покупок в Firestore, а также все классы для этих покупок.

Настройте доступ к Firebase

Чтобы получить доступ к Firebase Firestore, вам понадобится ключ доступа к учетной записи службы. Создайте его, открыв настройки проекта Firebase, перейдите в раздел «Учетные записи служб» , затем выберите « Создать новый закрытый ключ» .

27590fc77ae94ad4.png

Скопируйте загруженный файл JSON в папку assets/ и переименуйте его в service-account-firebase.json .

Настройте доступ к Google Play

Чтобы получить доступ к Play Store для проверки покупок, вам необходимо создать учетную запись службы с этими разрешениями и загрузить для нее учетные данные JSON.

  1. Перейдите в консоль Google Play и начните со страницы «Все приложения» .
  2. Откройте «Настройка» > «Доступ к API» . 317fdfb54921f50e.png Если консоль Google Play запрашивает создание существующего проекта или ссылку на него, сначала сделайте это, а затем вернитесь на эту страницу.
  3. Найдите раздел, в котором вы можете определить учетные записи служб, и нажмите « Создать новую учетную запись службы». 1e70d3f8d794bebb.png
  4. Нажмите ссылку Google Cloud Platform в появившемся диалоговом окне. 7c9536336dd9e9b4.png
  5. Выберите свой проект. Если вы его не видите, убедитесь, что вы вошли в правильную учетную запись Google в раскрывающемся списке «Учетная запись» в правом верхнем углу. 3fb3a25bad803063.png
  6. После выбора проекта нажмите + Создать учетную запись службы в верхней строке меню. 62fe4c3f8644acd8.png
  7. Укажите имя учетной записи службы, при необходимости укажите описание, чтобы вы могли запомнить, для чего она нужна, и перейдите к следующему шагу. 8a92d5d6a3dff48c.png
  8. Назначьте учетной записи службы роль редактора . 6052b7753667ed1a.png
  9. Завершите работу мастера, вернитесь на страницу доступа к API в консоли разработчика и нажмите « Обновить учетные записи служб». Вы должны увидеть свою вновь созданную учетную запись в списке. 5895a7db8b4c7659.png
  10. Нажмите Предоставить доступ для вашей новой учетной записи службы.
  11. Прокрутите следующую страницу вниз до блока «Финансовые данные» . Выберите « Просмотр финансовых данных, заказов и ответов на опросы об отмене» и «Управление заказами и подписками» . 75b22d0201cf67e.png
  12. Нажмите Пригласить пользователя . 70ea0b1288c62a59.png
  13. Теперь, когда учетная запись настроена, вам просто нужно сгенерировать некоторые учетные данные. Вернувшись в облачную консоль, найдите свою учетную запись службы в списке учетных записей служб, щелкните три вертикальные точки и выберите «Управление ключами» . 853ee186b0e9954e.png
  14. Создайте новый ключ JSON и загрузите его. 2a33a55803f5299c.pngcb4bf48ebac0364e.png
  15. Переименуйте загруженный файл в service-account-google-play.json, и переместите его в каталог assets/ .

Еще одна вещь, которую нам нужно сделать, — это открыть lib/constants.dart, и заменить значение androidPackageId на идентификатор пакета, который вы выбрали для своего приложения Android.

Настройте доступ к Apple App Store

Чтобы получить доступ к App Store для проверки покупок, вам необходимо настроить общий секрет:

  1. Откройте App Store Connect .
  2. Перейдите в мои приложения и выберите свое приложение.
  3. В навигации по боковой панели перейдите в покупки в приложении> Управление .
  4. В правом верхнем углу списка нажмите «Общий секрет».
  5. Создайте новый секрет и скопируйте его.
  6. Откройте lib/constants.dart, и замените значение appStoreSharedSecret только что вы только что сгенерировали.

D8B8042470AAEFF.PNG

B72F4565750E2F40.png

Константы файла конфигурации

Прежде чем продолжить, убедитесь, что в файле lib/constants.dart настроены следующие константы:

  • androidPackageId : идентификатор пакета, используемый на Android. например, com.example.dashclicker
  • appStoreSharedSecret : общий секрет для доступа к App Store Connect для выполнения проверки покупки.
  • bundleId : идентификатор пакета, используемый на iOS. например, com.example.dashclicker

Вы можете игнорировать остальные константы на данный момент.

10. Проверьте покупки

Общий поток для проверки покупок аналогичен для iOS и Android.

Для обоих магазинов ваша приложение получает токен при совершении покупки.

Этот токен отправляется приложением в ваш бэкэнд -сервис, которая, в свою очередь, проверяет покупку на серверах соответствующего магазина, используя предоставленный токен.

Служба Бэкэнд может затем выбрать хранить покупку и ответить на заявку, была ли покупка действительной или нет.

Если сервис Backend выполняет проверку с хранилищами, а не с приложением, работающим на устройстве вашего пользователя, вы можете предотвратить получение пользователя к доступу к премиальным функциям, например, пересмотреть их системные часы.

Установите трепетую сторону

Настройка аутентификации

Когда вы собираетесь отправлять покупки в свой сервис бэкэнд, вы хотите убедиться, что пользователь аутентифицирован при совершении покупки. Большая часть логики аутентификации уже добавлена ​​для вас в стартовом проекте, вы просто должны убедиться, что в PurchasePage показана кнопка входа в систему, когда пользователь еще не вошел в систему. Добавьте следующий код в начало метода сборки PurchasePage :

lib/pages/buy_page.dart

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

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

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

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

Конечная точка проверки вызова из приложения

В приложении создайте функцию _verifyPurchase(PurchaseDetails purchaseDetails) , которая вызывает конечную точку /verifypurchase на бэкэнд DART, используя пост -вызов HTTP.

Отправьте выбранный магазин ( google_play для Play Store или app_store для App Store), serverVerificationData и productID . Сервер возвращает код состояния, указывающий, проверена ли покупка.

В постоянных приложениях настройте IP -адрес сервера на свой локальный IP -адрес машины.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

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

Добавьте firebaseNotifier с созданием DashPurchases в main.dart:

lib/main.dart

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

Добавьте Getter для пользователя в Firebasenotifier, чтобы вы могли передать идентификатор пользователя в функцию проверки покупки.

lib/logic/firebase_notifier.dart

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

Добавьте функцию _verifyPurchase в класс DashPurchases . Эта async функция возвращает логическое, указывающее, подтверждена ли покупка.

lib/logic/dash_purchases.dart

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

Вызовите функцию _verifyPurchase в _handlePurchase непосредственно перед применением покупки. Вы должны применить покупку только в случае ее проверки. В производственном приложении вы можете указать это, например, применить пробную подписку, когда магазин временно недоступен. Однако, для этого примера, держите его простым, и примените покупку только при успешной проверке покупки.

lib/logic/dash_purchases.dart

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

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

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

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

В приложении все теперь готово к проверке покупок.

Установите сервис Backend

Затем настройте облачную функцию для проверки покупок на бэкэнд.

Построить обработчики покупки

Поскольку поток проверки для обоих магазинов близок к идентичному, создайте абстрактный класс PurchaseHandler и отдельные реализации для каждого магазина.

BE50C207C5A2A519.png

Начните с добавления файла purchase_handler.dart в папку lib/ , где вы определяете абстрактный класс PurchaseHandler с двумя абстрактными методами для проверки двух различных видов покупок: подписки и не подписание.

lib/buy_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,
  });
}

Как видите, каждый метод требует трех параметров:

  • userId: идентификатор пользователя зарегистрированного, так что вы можете привязать покупки с пользователем.
  • productData: данные о продукте. Вы собираетесь определить это через минуту.
  • token: токен, предоставляемый пользователю магазином.

Кроме того, чтобы облегчить обработчики покупок в использовании, добавьте метод verifyPurchase() , который можно использовать как для подписки, так и для не подписания:

lib/buy_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,
        );
    }
  }

Теперь вы можете просто вызвать verifyPurchase для обоих случаев, но при этом иметь отдельные реализации!

Класс ProductData содержит основную информацию о различных покупаемых продуктах, которая включает идентификатор продукта (иногда также называемый SKU) и ProductType .

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType может быть либо подпиской, либо не подписанной.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

Наконец, список продуктов определяется как карта в том же файле.

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

Затем определите некоторые реализации заполнителей для Google Play Store и Apple App Store. Начните с Google Play:

Создайте lib/google_play_purchase_handler.dart и добавьте класс, который расширяет 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;
  }
}

На данный момент он возвращает true для методов обработчика; Вы доберетесь до них позже.

Как вы могли бы заметить, конструктор берет экземпляр IapRepository . Обработчик покупки использует этот экземпляр для хранения информации о покупках в Firestore в дальнейшем. Чтобы общаться с Google Play, вы используете предоставленный AndroidPublisherApi .

Затем сделайте то же самое для обработчика магазина приложений. Создайте lib/app_store_purchase_handler.dart и добавьте класс, который снова расширяет PurchaseHandler :

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

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

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

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

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

Большой! Теперь у вас есть два обработчика покупки. Далее, давайте создадим конечную точку API API.

Используйте обработчики покупки

Откройте bin/server.dart и создайте конечную точку API с помощью shelf_route :

bin/server.dart

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router);
}

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

Приведенный выше код выполняет следующее:

  1. Определите конечную точку после приложения, которое вы создали ранее.
  2. Декодировать полезную нагрузку JSON и извлечь следующую информацию:
  3. userId : в настоящее время в системе пользователя идентификатор пользователя
  4. source : хранить используемый, либо app_store , либо google_play .
  5. productData : Получен из созданного вами productDataMap , который вы создали ранее.
  6. token : содержит данные проверки для отправки в магазины.
  7. Вызовите метод verifyPurchase , либо для GooglePlayPurchaseHandler , либо AppStorePurchaseHandler , в зависимости от источника.
  8. Если проверка была успешной, метод возвращает Response.ok OK клиенту.
  9. Если проверка не удается, метод возвращает Response.internalServerError к клиенту.

После создания конечной точки API вам необходимо настроить две обработчики покупки. Это требует от вас загрузки ключей учетной записи службы, полученных на предыдущем шаге, и настроить доступ к различным службам, включая API Android Publisher и API Firebase Firestore. Затем создайте два обработчика покупки с различными зависимостями:

bin/server.dart

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

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

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

Проверьте покупки Android: внедрить ручной покупки

Далее, продолжайте реализовать обработчик покупки Google Play.

Google уже предоставляет пакеты DART для взаимодействия с API, необходимыми для проверки покупок. Вы инициализировали их в файле server.dart и теперь используете их в классе GooglePlayPurchaseHandler .

Реализовать обработчик для покупок типа не подписания:

lib/Google_Play_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

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

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

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

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

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

Вы можете обновить обработчик покупки подписки аналогичным образом:

lib/Google_Play_purchase_handler.dart

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

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

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

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

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

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

Добавьте следующий метод, чтобы облегчить анализ идентификаторов заказа, а также два метода для анализа статуса покупки.

lib/Google_Play_purchase_handler.dart

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

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

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

Ваши покупки Google Play теперь должны быть проверены и хранятся в базе данных.

Затем перейдите к покупкам App Store для iOS.

Проверьте покупки iOS: реализуйте обработчик покупки

Для проверки покупок в App Store существует сторонний пакет DART с именем app_store_server_sdk , который облегчает процесс.

Начните с создания экземпляра ITunesApi . Используйте конфигурацию песочницы, а также включите регистрацию для облегчения отладки ошибок.

lib/app_store_purchase_handler.dart

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

Теперь, в отличие от API Google Play, App Store использует одни и те же конечные точки API как для подписок, так и для не подписанных. Это означает, что вы можете использовать одну и ту же логику для обеих обработчиков. Объединить их вместе, чтобы они называли ту же реализацию:

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 {
   //..
  }

Теперь реализуйте 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;
    }
  }

Закупки вашего магазина приложений теперь должны быть проверены и хранятся в базе данных!

Запустите бэкэнд

На этом этапе вы можете запустить dart bin/server.dart для обслуживания конечной точки /verifypurchase .

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

11. Следите за покупками

Рекомендуемый способ отслеживать покупки ваших пользователей находится в бэкэнд -сервисе. Это связано с тем, что ваш бэкэнд может реагировать на события из магазина и, следовательно, менее склонна к столкновению устаревшей информации из -за кэширования, а также менее восприимчиво к тому, чтобы их подделали.

Во -первых, установите обработку мероприятий магазина на бэкэнд с бэкэнд DART, который вы строили.

Процесс магазина на бэкэнд

Магазины имеют возможность сообщить ваш бэкэнд о любых случаях, которые происходят, например, когда подписки обновляются. Вы можете обработать эти события в бэкэнде, чтобы сохранить покупки в вашей базе данных. В этом разделе настройте это как для Google Play Store, так и для Apple App Store.

Обработка Google Play Billing Events

Google Play предоставляет биллинговые события через то, что они называют облачным пабом/подменной темой . По сути, это очереди сообщения, о которых могут быть опубликованы сообщения, а также потребляются.

Поскольку это функциональность, специфичная для Google Play, вы включаете эту функциональность в GooglePlayPurchaseHandler .

Начните с открытия lib/google_play_purchase_handler.dart и добавления импорта PubSubapi:

lib/Google_Play_purchase_handler.dart

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

Затем передайте PubsubApi в GooglePlayPurchaseHandler и измените конструктор класса, чтобы создать Timer следующим образом:

lib/Google_Play_purchase_handler.dart

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

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

Timer настроен для вызова метода _pullMessageFromSubSub каждые десять секунд. Вы можете отрегулировать продолжительность до собственного предпочтения.

Затем создайте _pullMessageFromSubSub

lib/Google_Play_purchase_handler.dart

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

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

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

Код, который вы только что добавили, связывается с пабом/суб -темой из Google Cloud каждые десять секунд и просит новые сообщения. Затем обрабатывает каждое сообщение в методе _processMessage .

Этот метод декодирует входящие сообщения и получает обновленную информацию о каждой покупке, как подписке, так и не подписания, вызывая существующую handleSubscription или handleNonSubscription , если это необходимо.

Каждое сообщение должно быть подтверждено с помощью метода _askMessage .

Затем добавьте необходимые зависимости в файл server.dart . Добавьте Pubsubapi.cloudplatformscope в конфигурацию учетных данных:

bin/server.dart

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

Затем создайте экземпляр Pubsubapi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

И, наконец, передайте его конструктору GooglePlayPurchaseHandler :

bin/server.dart

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

Google Play Setup

Вы написали код для употребления биллинговых событий из паба/суб -темы, но вы не создали паб/подменную тему, а также не публикуете какие -либо биллинговые мероприятия. Пришло время настроить это.

Во -первых, создайте паб/суб -тему:

  1. Посетите страницу Cloud Pub/Subs на консоли Google Cloud.
  2. Убедитесь, что вы находитесь в своем проекте Firebase и нажмите + Создать тему . D5EBF6897A0A8BF5.PNG
  3. Дайте новой теме имя, идентичное значениям для GOOGLE_PLAY_PUBSUB_BILLING_TOPIC в constants.ts . В этом случае назовите это play_billing . Если вы выберете что -то еще, обязательно обновите constants.ts . Создайте тему. 20d690fc543c4212.png
  4. В списке ваших тем по пабе/подразделениям нажмите три вертикальные точки для темы, которую вы только что создали, и нажмите « Просмотреть разрешения» . EA03308190609FB.PNG
  5. На боковой панели справа выберите «Добавить принципал» .
  6. Здесь добавьте google-play-developer-notifications@system.gserviceaccount.com и дайте ему роль Pub/Sub Publisher . 55631EC0549215BC.PNG
  7. Сохранить изменения разрешения.
  8. Скопируйте название темы темы, которую вы только что создали.
  9. Откройте воспроизведение консоли снова и выберите ваше приложение из списка All Apps .
  10. Прокрутите вниз и перейдите к монетизации> монетизации .
  11. Заполните полную тему и сохраните ваши изменения. 7e5e875dc6ce5d54.png

Все события Google Play Billing теперь будут опубликованы по теме.

Процесс App Store Billing Events

Затем сделайте то же самое для выставления счетов в магазине приложений. Существует два эффективных способа реализации обновлений в покупках для App Store. Одним из них является реализация веб -крюк, который вы предоставляете Apple, и они используют для связи с вашим сервером. Второй способ, который вы найдете в этом коделабе, - это подключение к API App Store Server и получение информации о подписке вручную.

Причина, по которой этот CodeLab фокусируется на втором решении, заключается в том, что вам придется разоблачить свой сервер в Интернете, чтобы реализовать веб -крючок.

В производственной среде в идеале вы хотели бы иметь оба. WebHook для получения событий из App Store и API сервера в случае пропустить событие или необходимо дважды проверить статус подписки.

Начните с открытия lib/app_store_purchase_handler.dart и добавив зависимость AppSotoreserVerapi:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

AppStorePurchaseHandler(
  this.iapRepository,
  this.appStoreServerAPI, // new
)

Измените конструктор, чтобы добавить таймер, который вызовет метод _pullStatus . Этот таймер будет вызывать метод _pullStatus каждые 10 секунд. Вы можете отрегулировать этот таймер продолжительностью к вашим потребностям.

lib/app_store_purchase_handler.dart

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

Затем создайте метод _pullstatus следующим образом:

lib/app_store_purchase_handler.dart

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

Этот метод работает следующим образом:

  1. Получает список активных подписок от Firestore с использованием iAprepository.
  2. Для каждого порядка он запрашивает состояние подписки на API App Store Server API.
  3. Получает последнюю транзакцию для этой покупки подписки.
  4. Проверяет дату истечения срока действия.
  5. Обновляет статус подписки на Firestore, если он истек, он будет помечен как таковой.

Наконец, добавьте весь необходимый код для настройки API API API App Server:

bin/server.dart

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

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

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

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


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

Настройка магазина приложений

Далее настройте App Store:

  1. Войдите в систему в App Store Connect , и выберите пользователей и доступ .
  2. Перейдите в тип ключа> Покупка в приложении .
  3. Нажмите на значок «Plus», чтобы добавить новый.
  4. Дайте ему имя, например, "CodeLab Key".
  5. Загрузите файл P8, содержащий ключ.
  6. Скопируйте его в папку Assets, с помощью name SubscriptionKey.p8 .
  7. Скопируйте идентификатор ключа из вновь созданного ключа и установите его на постоянную appStoreKeyId в файле lib/constants.dart .
  8. Скопируйте идентификатор эмитента прямо в верхней части списка клавиш и установите его на постоянную appStoreIssuerId в файле lib/constants.dart .

9540ea9ada3da151.png

Отслеживать покупки на устройстве

Наиболее безопасным способом отслеживания ваших покупок является на стороне сервера, потому что клиент трудно обеспечить, но вам необходимо иметь некоторый способ вернуть информацию к клиенту, чтобы приложение могло действовать в отношении информации о состоянии подписки. Хранив покупки в Firestore, вы можете легко синхронизировать данные с клиентом и автоматически обновлять их.

Вы уже включили IAprepo в приложение, которое является хранилищем Firestore, который содержит все данные о покупке пользователя в List<PastPurchase> purchases . Репозиторий также содержит hasActiveSubscription, что верно, когда есть покупка с productId storeKeySubscription со статусом, который не истек. Когда пользователь не вошел в систему, список пуст.

lib/repo/iap_repo.dart

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

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

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

      notifyListeners();
    });
  }

Вся логика покупки находится в классе DashPurchases , где следует применяться или удалять подписки. Итак, добавьте iapRepo в качестве свойства в классе и назначьте iapRepo в конструкторе. Затем напрямую добавьте слушателя в конструктор и удалите слушателя в методе dispose() . Сначала слушатель может быть просто пустой функцией. Поскольку IAPRepo является ChangeNotifier , и вы называете notifyListeners() каждый раз, когда закупки в Firestore изменяются, метод purchasesUpdate() всегда вызывается при изменении приобретенных продуктов.

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

Затем поставьте IAPRepo конструктору в main.dart. Вы можете получить репозиторий, используя context.read потому что он уже создан в Provider .

lib/main.dart

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

Далее напишите код для функции purchaseUpdate() . В dash_counter.dart, applyPaidMultiplier и removePaidMultiplier методы PAIDMULTIPLIER Установите множитель на 10 или 1 соответственно, поэтому вам не нужно проверять, является ли подписка уже применяется. При изменении состояния подписки вы также обновляете статус покупаемого продукта, чтобы на странице покупки вы могли показать, что он уже активен. Установите свойство _beautifiedDashUpgrade на основе того, куплен ли обновление.

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

Теперь вы гарантировали, что подписка и статус обновления всегда актуальны в бэкэнд -службе и синхронизированы с приложением. Приложение действует соответствующим образом и применяет функции подписки и обновления в вашу игру Dash Clicker.

12. Все сделано!

Поздравляю!!! Вы завершили CodeLab. Вы можете найти завершенный код для этого коделаба в android_studio_folder.png Полная папка.

Чтобы узнать больше, попробуйте другие трепетные коделабы .