1. Введение
Последнее обновление: 11 июля 2023 г.
Для добавления покупок из приложения в приложение Flutter требуется правильная настройка магазинов App и Play, проверка покупки и предоставление необходимых разрешений, таких как льготы по подписке.
В этой лаборатории кода вы добавите в приложение (предоставленное вам) три типа покупок внутри приложения и подтвердите эти покупки с помощью серверной части Dart с Firebase. Прилагаемое приложение Dash Clicker содержит игру, в которой в качестве валюты используется талисман Dash. Вы добавите следующие варианты покупки:
- Возможность повторной покупки сразу за 2000 Dash.
- Единовременная покупка обновления, позволяющая превратить старый стиль Dash в современный Dash.
- Подписка, которая удваивает автоматически генерируемые клики.
Первый вариант покупки дает пользователю прямую выгоду в размере 2000 Dash. Они доступны непосредственно пользователю и могут быть куплены много раз. Это называется расходным материалом, поскольку он потребляется напрямую и может потребляться несколько раз.
Второй вариант делает Dash более красивым. Его нужно купить только один раз, и он доступен навсегда. Такая покупка называется непотребляемой, поскольку она не может быть использована приложением, но действительна навсегда.
Третий и последний вариант покупки — подписка. Пока подписка активна, пользователь будет получать Dash быстрее, но когда он перестанет платить за подписку, преимущества также пропадут.
Серверная служба (также предоставляемая вам) работает как приложение Dart, проверяет совершение покупок и сохраняет их с помощью Firestore. Firestore используется для упрощения процесса, но в рабочем приложении вы можете использовать любой тип серверной службы.
Что ты построишь
- Вы расширите приложение для поддержки покупок расходных материалов и подписок.
- Вы также расширите серверное приложение 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.
В структуре папок Xcode проект Runner находится вверху, а целевые объекты Flutter , Runner и Products находятся под проектом Runner. Дважды щелкните Runner, чтобы изменить настройки проекта, и нажмите «Подписание и возможности» . Введите идентификатор пакета, который вы только что выбрали, в поле «Команда» , чтобы указать свою команду.
Теперь вы можете закрыть Xcode и вернуться в Android Studio, чтобы завершить настройку Android. Для этого откройте файл build.gradle
в разделе android/app,
и измените свой applicationId
(строка 37 на снимке экрана ниже) на идентификатор приложения, такой же, как идентификатор пакета iOS. Обратите внимание, что идентификаторы магазинов iOS и Android не обязательно должны быть идентичными, однако сохранение их идентичности снижает вероятность ошибок, поэтому в этой лаборатории кода мы также будем использовать идентичные идентификаторы.
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/ и нажмите «Соглашения, налоги и банковские операции» .
Здесь вы увидите соглашения для бесплатных и платных приложений. Статус бесплатных приложений должен быть активным, а статус платных — новым. Обязательно ознакомьтесь с условиями, примите их и введите всю необходимую информацию.
Если все настроено правильно, статус платных приложений будет активен. Это очень важно, поскольку вы не сможете опробовать покупки в приложении без активного соглашения.
Зарегистрировать идентификатор приложения
Создайте новый идентификатор на портале разработчиков Apple.
Выберите идентификаторы приложений
Выберите приложение
Предоставьте некоторое описание и установите идентификатор пакета, чтобы он соответствовал тому же значению, которое было ранее установлено в XCode.
Дополнительные инструкции о том, как создать новый идентификатор приложения, см. в справке по учетной записи разработчика .
Создание нового приложения
Создайте новое приложение в App Store Connect, используя свой уникальный идентификатор пакета.
Дополнительные инструкции о том, как создать новое приложение и управлять соглашениями, см. в справке App Store Connect .
Чтобы протестировать покупки в приложении, вам понадобится тестовый пользователь песочницы. Этот тестовый пользователь не должен быть подключен к iTunes — он используется только для тестирования покупок в приложении. Вы не можете использовать адрес электронной почты, который уже используется для учетной записи Apple. В разделе «Пользователи и доступ» перейдите в раздел «Тестеры» в разделе «Песочница» , чтобы создать новую учетную запись «песочницы» или управлять существующими идентификаторами Apple ID «песочницы».
Теперь вы можете настроить пользователя песочницы на своем iPhone, выбрав «Настройки» > «App Store» > «Учетная запись песочницы».
Настройка покупок в приложении
Теперь вы настроите три покупаемых предмета:
-
dash_consumable_2k
: покупка расходных материалов, которую можно приобретать много раз, которая дает пользователю 2000 тире (валюта в приложении) за каждую покупку. -
dash_upgrade_3d
: нерасходуемая покупка «обновления», которую можно приобрести только один раз, и которая дает пользователю косметически другой тир, который можно щелкнуть. -
dash_subscription_doubler
: подписка, которая предоставляет пользователю вдвое больше Dash за клик в течение всего срока действия подписки.
Откройте «Покупки в приложении» > «Управление» .
Создайте свои покупки в приложении с указанными идентификаторами:
- Настройте
dash_consumable_2k
в качестве расходного материала .
Используйте dash_consumable_2k
в качестве идентификатора продукта. Ссылочное имя используется только при подключении к магазину приложений, просто установите для него dash consumable 2k
и добавьте свои локализации для покупки. Позвоните о покупке Spring is in the air
, из описания 2000 dashes fly out
.
- Настройте
dash_upgrade_3d
как нерасходуемый файл .
Используйте dash_upgrade_3d
в качестве идентификатора продукта. Установите ссылочное имя dash upgrade 3d
и добавьте свои локализации для покупки. Позвоните о покупке 3D Dash
с Brings your dash back to the future
как описано в описании.
- Настройте
dash_subscription_doubler
как подписку с автоматическим продлением .
Схема подписки немного другая. Сначала вам нужно будет установить справочное имя и идентификатор продукта:
Далее вам необходимо создать группу подписки. Если несколько подписок входят в одну группу, пользователь может подписаться только на одну из них одновременно, но может легко повысить или понизить версию одной из этих подписок. Просто позвоните в эту группу subscriptions
.
Далее введите продолжительность подписки и локализации. Назовите эту подписку Jet Engine
с описанием. Doubles your clicks
. Нажмите Сохранить .
После того, как вы нажали кнопку «Сохранить» , добавьте стоимость подписки. Выбирайте любую цену по вашему желанию.
Теперь вы должны увидеть три покупки в списке покупок:
5. Настройте Play Маркет.
Как и в случае с App Store, вам также понадобится учетная запись разработчика для Play Store. Если у вас его еще нет, зарегистрируйте аккаунт .
Создать новое приложение
Создайте новое приложение в консоли Google Play:
- Откройте консоль Play .
- Выберите Все приложения > Создать приложение.
- Выберите язык по умолчанию и добавьте название для своего приложения. Введите название своего приложения так, как вы хотите, чтобы оно отображалось в Google Play. Вы можете изменить имя позже.
- Укажите, что ваше приложение является игрой. Вы можете изменить это позже.
- Укажите, является ли ваше приложение бесплатным или платным.
- Добавьте адрес электронной почты, который пользователи Play Store смогут использовать, чтобы связаться с вами по поводу этого приложения.
- Заполните правила содержания и декларации законов США об экспорте.
- Выберите Создать приложение .
После создания приложения перейдите на панель управления и выполните все задачи в разделе «Настройка приложения» . Здесь вы предоставляете некоторую информацию о своем приложении, например рейтинги контента и снимки экрана.
Подпишите заявку
Чтобы иметь возможность протестировать покупки в приложении, вам необходимо загрузить хотя бы одну сборку в 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», чтобы согласиться.
Затем загрузите пакет приложения app-release.aab
, созданный командой сборки.
Нажмите «Сохранить» , а затем нажмите «Просмотреть выпуск».
Наконец, нажмите «Начать развертывание внутреннего тестирования» , чтобы активировать версию для внутреннего тестирования.
Настройка тестовых пользователей
Чтобы иметь возможность тестировать покупки в приложении, учетные записи Google ваших тестировщиков должны быть добавлены в консоль Google Play в двух местах:
- К конкретному тестовому треку (Внутреннее тестирование)
- В качестве тестера лицензий
Сначала начните с добавления тестера в трек внутреннего тестирования. Вернитесь в раздел «Релиз» > «Тестирование» > «Внутреннее тестирование» и перейдите на вкладку «Тестеры» .
Создайте новый список адресов электронной почты, нажав Создать список адресов электронной почты . Дайте списку имя и добавьте адреса электронной почты учетных записей Google, которым необходим доступ для тестирования покупок в приложении.
Далее установите флажок рядом со списком и нажмите Сохранить изменения .
Затем добавьте тестеры лицензий:
- Вернитесь к представлению «Все приложения» консоли Google Play.
- Откройте «Настройки» > «Проверка лицензии» .
- Добавьте те же адреса электронной почты тестировщиков, которые должны иметь возможность тестировать покупки в приложении.
- Установите ответ лицензии на
RESPOND_NORMALLY
. - Нажмите Сохранить изменения.
Настройка покупок в приложении
Теперь вы настроите предметы, которые можно приобрести в приложении.
Как и в App Store, вам необходимо определить три разные покупки:
-
dash_consumable_2k
: покупка расходных материалов, которую можно приобретать много раз, которая дает пользователю 2000 тире (валюта в приложении) за каждую покупку. -
dash_upgrade_3d
: нерасходуемая покупка «обновления», которую можно приобрести только один раз, что дает пользователю косметически другой тир, который можно щелкнуть. -
dash_subscription_doubler
: подписка, которая предоставляет пользователю вдвое больше Dash за клик в течение всего срока действия подписки.
Во-первых, добавьте расходные и нерасходные материалы.
- Перейдите в консоль Google Play и выберите свое приложение.
- Откройте «Монетизация» > «Продукты» > «Продукты для продажи в приложении» .
- Нажмите Создать продукт.
- Введите всю необходимую информацию о вашем продукте. Убедитесь, что идентификатор продукта точно соответствует идентификатору, который вы собираетесь использовать.
- Нажмите «Сохранить».
- Нажмите «Активировать» .
- Повторите процедуру для покупки нерасходуемого «обновления».
Далее добавляем подписку:
- Перейдите в консоль Google Play и выберите свое приложение.
- Откройте «Монетизация» > «Продукты» > «Подписки» .
- Нажмите Создать подписку.
- Введите всю необходимую информацию для вашей подписки. Убедитесь, что идентификатор продукта точно соответствует идентификатору, который вы собираетесь использовать.
- Нажмите « Сохранить».
Теперь ваши покупки должны быть настроены в Play Console.
6. Настройте Firebase
В этой лаборатории кода вы будете использовать серверную службу для проверки и отслеживания покупок пользователей.
Использование серверной службы имеет ряд преимуществ:
- Вы можете безопасно проверять транзакции.
- Вы можете реагировать на события выставления счетов из магазинов приложений.
- Вы можете отслеживать покупки в базе данных.
- Пользователи не смогут обманом заставить ваше приложение предоставить дополнительные функции, переведя системные часы назад.
Хотя существует множество способов настроить серверную службу, вы сделаете это с помощью облачных функций и Firestore, используя собственную Firebase от Google.
Написание серверной части выходит за рамки этой лаборатории, поэтому стартовый код уже включает проект Firebase, который обрабатывает базовые покупки, чтобы вы могли начать работу.
Плагины Firebase также включены в стартовое приложение.
Вам осталось создать собственный проект Firebase, настроить приложение и серверную часть для Firebase и, наконец, развернуть серверную часть.
Создать проект Firebase
Перейдите в консоль Firebase и создайте новый проект Firebase. В этом примере вызовите проект Dash Clicker.
В backend-приложении вы привязываете покупки к конкретному пользователю, поэтому вам нужна аутентификация. Для этого используйте модуль аутентификации Firebase со входом в Google.
- На панели управления Firebase перейдите в раздел «Аутентификация» и включите ее, если необходимо.
- Перейдите на вкладку «Метод входа» и включите поставщика входа в систему Google .
Поскольку вы также будете использовать базу данных Firestore Firebase, включите и ее.
Установите правила 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) .
Чтобы разрешить вход в 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
.
Пара ключ-значение уже добавлена, но их значения необходимо заменить:
- Получите значение
REVERSED_CLIENT_ID
из файлаGoogleService-Info.plist
без окружающего его элемента<string>..</string>
. - Замените значение в файлах
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, если они настроены правильно. Обратите внимание, что может пройти некоторое время, прежде чем покупки станут доступны при вводе в соответствующие консоли.
Вернитесь к 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, перейдите в раздел «Учетные записи служб» , затем выберите « Создать новый закрытый ключ» .
Скопируйте загруженный файл JSON в папку assets/
и переименуйте его в service-account-firebase.json
.
Настройте доступ к Google Play
Чтобы получить доступ к Play Store для проверки покупок, вам необходимо создать учетную запись службы с этими разрешениями и загрузить для нее учетные данные JSON.
- Перейдите в консоль Google Play и начните со страницы «Все приложения» .
- Откройте «Настройка» > «Доступ к API» . Если консоль Google Play запрашивает создание существующего проекта или ссылку на него, сначала сделайте это, а затем вернитесь на эту страницу.
- Найдите раздел, в котором вы можете определить учетные записи служб, и нажмите « Создать новую учетную запись службы».
- Нажмите ссылку Google Cloud Platform в появившемся диалоговом окне.
- Выберите свой проект. Если вы его не видите, убедитесь, что вы вошли в правильную учетную запись Google в раскрывающемся списке «Учетная запись» в правом верхнем углу.
- После выбора проекта нажмите + Создать учетную запись службы в верхней строке меню.
- Укажите имя учетной записи службы, при необходимости укажите описание, чтобы вы могли запомнить, для чего она нужна, и перейдите к следующему шагу.
- Назначьте учетной записи службы роль редактора .
- Завершите работу мастера, вернитесь на страницу доступа к API в консоли разработчика и нажмите « Обновить учетные записи служб». Вы должны увидеть свою вновь созданную учетную запись в списке.
- Нажмите Предоставить доступ для вашей новой учетной записи службы.
- Прокрутите следующую страницу вниз до блока «Финансовые данные» . Выберите « Просмотр финансовых данных, заказов и ответов на опросы об отмене» и «Управление заказами и подписками» .
- Нажмите Пригласить пользователя .
- Теперь, когда учетная запись настроена, вам просто нужно сгенерировать некоторые учетные данные. Вернувшись в облачную консоль, найдите свою учетную запись службы в списке учетных записей служб, щелкните три вертикальные точки и выберите «Управление ключами» .
- Создайте новый ключ JSON и загрузите его.
- Переименуйте загруженный файл в
service-account-google-play.json,
и переместите его в каталогassets/
.
Еще одна вещь, которую нам нужно сделать, — это открыть lib/constants.dart,
и заменить значение androidPackageId
на идентификатор пакета, который вы выбрали для своего приложения Android.
Настройте доступ к Apple App Store
Чтобы получить доступ к App Store для проверки покупок, вам необходимо настроить общий секрет:
- Откройте App Store Connect .
- Перейдите в мои приложения и выберите свое приложение.
- В навигации по боковой панели перейдите в покупки в приложении> Управление .
- В правом верхнем углу списка нажмите «Общий секрет».
- Создайте новый секрет и скопируйте его.
- Откройте
lib/constants.dart,
и замените значениеappStoreSharedSecret
только что вы только что сгенерировали.
Константы файла конфигурации
Прежде чем продолжить, убедитесь, что в файле 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
и отдельные реализации для каждого магазина.
Начните с добавления файла 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');
}
}
Приведенный выше код выполняет следующее:
- Определите конечную точку после приложения, которое вы создали ранее.
- Декодировать полезную нагрузку JSON и извлечь следующую информацию:
-
userId
: в настоящее время в системе пользователя идентификатор пользователя -
source
: хранить используемый, либоapp_store
, либоgoogle_play
. -
productData
: Получен из созданного вамиproductDataMap
, который вы создали ранее. -
token
: содержит данные проверки для отправки в магазины. - Вызовите метод
verifyPurchase
, либо дляGooglePlayPurchaseHandler
, либоAppStorePurchaseHandler
, в зависимости от источника. - Если проверка была успешной, метод возвращает
Response.ok
OK клиенту. - Если проверка не удается, метод возвращает
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
Вы написали код для употребления биллинговых событий из паба/суб -темы, но вы не создали паб/подменную тему, а также не публикуете какие -либо биллинговые мероприятия. Пришло время настроить это.
Во -первых, создайте паб/суб -тему:
- Посетите страницу Cloud Pub/Subs на консоли Google Cloud.
- Убедитесь, что вы находитесь в своем проекте Firebase и нажмите + Создать тему .
- Дайте новой теме имя, идентичное значениям для
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
вconstants.ts
. В этом случае назовите этоplay_billing
. Если вы выберете что -то еще, обязательно обновитеconstants.ts
. Создайте тему. - В списке ваших тем по пабе/подразделениям нажмите три вертикальные точки для темы, которую вы только что создали, и нажмите « Просмотреть разрешения» .
- На боковой панели справа выберите «Добавить принципал» .
- Здесь добавьте
google-play-developer-notifications@system.gserviceaccount.com
и дайте ему роль Pub/Sub Publisher . - Сохранить изменения разрешения.
- Скопируйте название темы темы, которую вы только что создали.
- Откройте воспроизведение консоли снова и выберите ваше приложение из списка All Apps .
- Прокрутите вниз и перейдите к монетизации> монетизации .
- Заполните полную тему и сохраните ваши изменения.
Все события 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,
));
}
}
}
}
Этот метод работает следующим образом:
- Получает список активных подписок от Firestore с использованием iAprepository.
- Для каждого порядка он запрашивает состояние подписки на API App Store Server API.
- Получает последнюю транзакцию для этой покупки подписки.
- Проверяет дату истечения срока действия.
- Обновляет статус подписки на 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:
- Войдите в систему в App Store Connect , и выберите пользователей и доступ .
- Перейдите в тип ключа> Покупка в приложении .
- Нажмите на значок «Plus», чтобы добавить новый.
- Дайте ему имя, например, "CodeLab Key".
- Загрузите файл P8, содержащий ключ.
- Скопируйте его в папку Assets, с помощью name
SubscriptionKey.p8
. - Скопируйте идентификатор ключа из вновь созданного ключа и установите его на постоянную
appStoreKeyId
в файлеlib/constants.dart
. - Скопируйте идентификатор эмитента прямо в верхней части списка клавиш и установите его на постоянную
appStoreIssuerId
в файлеlib/constants.dart
.
Отслеживать покупки на устройстве
Наиболее безопасным способом отслеживания ваших покупок является на стороне сервера, потому что клиент трудно обеспечить, но вам необходимо иметь некоторый способ вернуть информацию к клиенту, чтобы приложение могло действовать в отношении информации о состоянии подписки. Хранив покупки в 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. Вы можете найти завершенный код для этого коделаба в Полная папка.
Чтобы узнать больше, попробуйте другие трепетные коделабы .