Привет, меня зовут Алексей, я разрабатываю Adapty SDK для Flutter. Сегодня я расскажу про внедрение внутренних покупок в мобильное приложение на Flutter с помощью плагина, который мы разрабатываем.
Flutter — это относительно новый фреймворк от Google для быстрого создания кросс-платформенных приложений. Второй популярный фреймворк — React Native, о покупках на Реакте мы писали в другой статье.
Приложения на Flutter собираются сразу и под iOS, и под Android, следовательно, библиотека для покупок должна поддерживать и StoreKit, и Billing Library.
Архитектурно каждый плагин для платежей, включая наш Adapty Flutter SDK представляет собой обертку над нативными библиотеками StoreKit и Billing Library. В нашем случае мы делали обертку над своими же библиотеками Adapty iOS и Android SDK.
Open source библиотеки для подключения внутренних покупок на Flutter
Самые популярные решения для подключения покупок на Flutter — это open source плагины in_app_purchase и flutter_inapp_purchase. Первая — официальный плагин от команды разработчиков Flutter, второй — неофициальный.
Эти плагины созданы для клиентской части платежей, то есть они не предоставляют серверную верификацию покупок (server-side receipt validation).Вам нужно самим создать инфраструктуру со стороны сервера для валидации чеков, сбора аналитики по событиям платежей: отслеживания продлений, возвратов, триалов, отмен и т.д.
Также поддержка новых фичей, которые выкатывают сторы, появляется в этих библиотеках довольно поздно. Например, сейчас в них нет промо-офферов, pay as you go и pay upfront от iOS.
Так как наша библиотека работает с нашим сервером, то она сразу закрывает следующие технические задачи:
серверная валидация покупок;
поддержка всех актуальные нативные фичи по платежам;
DRY синтаксис;
сбор аналитики подписок и покупок;
быстрый запуск экспериментов с покупками, ценами и периодами, a/b тесты;
пейволлов и отправка скидок.
Чтобы сделать статью короче и удобнее, мы будем оставлять ссылки на предварительные шаги на нативных платформах из наших прошлых статей.
Создание покупок в iOS и в Android
Для начала надо создать аккаунт разработчика, если у вас его нет, а дальше в iOS и в Android нужно будет создать по одной недельной покупке. Как это делается, мы описали в других статьях:
После этого можем приступить к созданию и настройке проекта.
Создание проекта и настройка
Чтобы покупки заработали и у нас в системе, и в вашей реализации, нужно заполнить технические параметры.

Для iOS нужно:
указать Bundle ID — чтобы все работало;
установить App Store Server Notifications в App Store Connect — для того, чтобы мы могли узнавать о событиях подписок;
заполнить App Store Shared Secret — чтобы валидировать рецепты.
Для Android обязательны Package Name и Service Account Key File. Package name — аналог Bundle ID с iOS. Нужно указать тот, что в коде, он находится в файле /android/app/build.gradle в разделе android.defaultConfig.applicationId.
Настройка продуктов и пейволлов
Продукт в Adapty инкапсулирует в себе продукты из разных магазинов. Сейчас это App Store и Google Play, но в будущем будут другие. Сделано это для более удобной сводной статистики и для удобной работы с сущностями более высокого уровня, а не с идентификаторами.
Пейволл — это абстрактная сущность, которая содержит в себе массив продуктов и remote config (JSON c любой мета-информацией от разработчика). В приложение зашивается идентификатор пейволла и по нему приходит вся информация о продуктах и конфиг. При этом в любой момент продукты можно изменить, как и конфиг. Стандартный сценарий работы — вы делаете свою верстку пейволла, а заполняете его информацией от нас.
Создание продукта
Создадим один продукт, соответствующий недельной подписке в Google Play Console и App Store Connect. Нужно заполнить ID соответствующих продуктов из платежных систем. Важно, что так как у App Store Connect нет API, это нужно сделать руками, но только один раз.

Создание пейволла
При создании пейволла самое главное — указать его ID в удобном и понятном формате. По этому идентификатору SDK будет запрашивать всю информацию о пейволле. Нам кажется удачным такой подход: в такой архитектуре не нужно хардкодить продукты на клиенте, появляется гибкость в управлении продуктами, версионировании, запуске тестов и тд. Как альтернатива такому подходу — использовать Firebase JSON c набором вшитых продуктов, но это менее удобно и не валидирует ошибки.

Все, мы создали продукт, пейволл и теперь готовы к первой покупке. Перейдем к установке SDK.
Работа с SDK
Рассмотрим основные функции, которые нам понадобятся для работы с подписками.
Установка библиотеки
В первую очередь нужно добавить библиотеку adapty_flutter в ваш проект. Для этого в файле pubspec.yaml добавляем зависимость:
dependencies:
adapty_flutter: ^1.0.4
И запустите
flutter pub get
После этого вы можете импортировать Adapty SDK в свое приложение следующим образом:
import 'package:adapty_flutter/adapty_flutter.dart';
Настройка
Чтобы ваше приложение могло работать с Adapty, вам необходимо его настроить. Для этого добавьте флаг AdaptyPublicSdkKey в Info.plist (iOS) или в AndroidManifest.xml (Android) с вашим публичным SDK ключом.
SDK ключ вы можете найти в настройках Adapty:

Info.plist:
<dict>
...
<key>AdaptyPublicSdkKey</key>
AndroidManifest.xml:
<application ...>
...
<meta-data
android:name="AdaptyPublicSdkKey"
android:value="PUBLIC_SDK_KEY" />
</application>
Далее активируйте SDK, вызвав на стороне Flutter следующий код, например, в методе main() вашего проекта:
void main() {
runZoned(() async {
Adapty.activate();
final installId = await Service.getOrCreateInstallId();
await Adapty.identify(***customer-user-id***);
await Adapty.setLogLevel(AdaptyLogLevel.verbose);
runApp(MyApp());
});
}
Функция void Adapty.activate() активирует библиотеку Adapty_Flutter:
Future<bool> Adapty.identify(String customerUserId)
Adapty.identify позволяет установить id пользователя. Adapty отправляет его в подписку и аналитику, чтобы присвоить события нужному профилю. Вы также сможете найти клиентов по customerUserId в Profiles.
Adapty регистрирует ошибки и другую важную информацию, чтобы помочь вам понять, что происходит. Функция
Future<void> Adapty.identify(AdaptyLogLevel value)
позволяет установить один из 3 возможных значений:
AdaptyLogLevel.none(по умолчанию): ничего не будет регистрироваться;AdaptyLogLevel.errors: будут регистрироваться только ошибки;AdaptyLogLevel.verbose: будут регистрироваться вызовы методов, запросы API/ответы и ошибки.
Получение пейволлов
Чтобы получить список пейволов, выполните следующий код:
try {
final GetPaywallsResult getPaywallsResult = await Adapty.getPaywalls(forceUpdate: Bool);
final List<AdaptyPaywall> paywalls = getPaywallsResult.paywalls;
} on AdaptyError(adaptyError) {}
catch(e) {}
Функция Adapty.getPaywalls() возвращает объект GetPaywallsResult который содержит:
пейволлы: массив пейволлов (
AdaptyPaywall). Модель содержит список продуктов, ID пейволла,custom payloadи несколько других значений.
Совершение покупок
После того, как на предыдущем шаге мы получили массив пейволов, требуется найти нужный нам, для того чтобы отобразить соответствующие продукты в интерфейсе (предположим, что наш пейвол в админке был назван your_paywall_id):
final List<AdaptyPaywall>? paywalls = getPaywallsResult.paywalls;
myPaywall = paywalls?.firstWhere((paywall) => paywall.developerId == "your_paywall_id", orElse: null);
Далее, воспользовавшись массивом продуктов из поля products, отображаем их все. Допустим, пользователь желает купить первый продукт, так что для простоты возьмем первый элемент массива продуктов:
final AdaptyProduct? product = myPaywall?.products?.first;
Для запуска процесса покупки вызываем функцию makePurchaseResult (не забыв обернуть в try-catch блок, чтобы получать все ошибки от sdk)
final MakePurchaseResult makePurchaseResult = await Adapty.makePurchase(product);
После того, как функция отработает успешно, в переменной makePurchaseResult будет результат. Проверяем наличие уровня доступа после завершения процесса покупки:
final isPremium = makePurchaseResult?.purchaserInfo?.accessLevels['premium']?.isActive ?? false;
примечание: AdaptyErrorCode.paymentCancelled указывает на то, что пользователь сам отменил покупку, и по сути не является ошибкой.
Для восстановления покупок используется метод .restorePurchases():
try {
final RestorePurchasesResult restorePurchasesResult = await Adapty.restorePurchases();
// "premium" is an identifier of default access level
if (restorePurchasesResult?.purchaserInfo?.accessLevels['premium']?.isActive ?? false) {
// grant access to premium features
}
} on AdaptyError catch (adaptyError) {}
catch (e) {}
Обратите внимание, что и объект MakePurchaseResult, и объект RestorePurchasesResult включают purchaserInfo. Этот объект содержит информацию об уровнях доступа, подписках и покупках без подписки. Как правило, вы должны проверить только статус уровня доступа, чтобы определить, имеет ли пользователь премиум-доступ к приложению.
Статус подписки
Чтобы не проверять предыдущую цепочку транзакций, мы оперируем понятием уровня доступа. Уровень доступа — эта флаг, который говорит о том, какого уровня функционал доступен пользователю в приложении. Если покупок не было, то уровень доступа пустой. Иначе, тот, который вы привязали к продукту.
Например, у вас может быть две уровня доступа: серебряный и золотой. Разные покупки разблокируют разный доступ и фичи. Большинство приложений имеют один уровень доступа.
Достаточно проверить, что у пользователя есть активный уровень доступа, чтобы понять, что подписка пользователя активна. Для этого используется метод .getPurchaserInfo()
try {
AdaptyPurchaserInfo purchaserInfo = await Adapty.getPurchaserInfo();
// "premium" is an identifier of default access level
if (purchaserInfo.accessLevels['premium']?.isActive ?? false) {
// grant access to premium features
}
} on AdaptyError catch (adaptyError) {}
catch (e) {}
Также вы можете узнавать об изменение статуса уровня доступа подписчика, подписавшись на стрим .purchaserInfoUpdateStream, как показано ниже:
Adapty.purchaserInfoUpdateStream.listen((purchaserInfo) {
print('#Adapty# Purchaser Info Updated');
if (purchaserInfo.accessLevels['premium'].isActive) {
// grant access to premium features
}
});
Заключение
Мы сделали SDK так, чтобы вы могли максимально быстро и гибко внедрить платежи к себе в приложение. Более того, мы постарались сделать дальнейшие шаги работы с платежами, такие как А/Б тесты, аналитика и интеграции максимально простыми и быстрыми.
Мы делаем полный функционал покупок бесплатно (если ваша выручка не превышает $10 000 в месяц). Сохраните себе месяцы работы и сфокусируйтесь на главном — на вашем продукте.
qbaddev
Все же, на kotlin более удобно все как-то скомпановано.