Привет, меня зовут Алексей. Сегодня я расскажу про внедрение внутренних покупок в мобильное приложение на Flutter с помощью плагина In-App Purchase.
Сначала расскажу немного о самих предметах которые мы можем продавать чтоб потом не было вопросов.
Есть 3 типа платного контента:
1. Расходуемые предметы (монеты, кристаллы, патроны и тп).
2. Нерасходуемые предметы (аватары, костюмы, и тп. т.е. все то что можно купить только один раз).
3. Подписки (ну тут все и так понятно).
Настройка iOS
Можете начать работу с изучения офф документации эпл, но она максимально не понятна для меня, поэтому я расскажу что надо сделать из своего опыта)
Первым делом идем в AppstoreConnect, там в верхнем меню выбираем agreements(Соглашения) и принимаем соглашение о "Платных приложениях и покупках в них". п.с. соглашение может принять только владелец аккаунта!
Далее мы едем на страницу приложения в сторе и в боковом меню выбираем пункт In-App Purchases или Subscriptions в зависимости от того какой продукт мы хотим продавать.
Я буду показывать на примере монет.
Выбираем пункт In-App Purchases и в открывшемся окне нажимаем + и видим такую картину.
Type - это расходуемый или нерасходуемый материал
Name - название отображаемое именно в AppStoreConnect
Id - уникальный идентификатор продукта для этого приложения
Нажимаем создать и видим окно с нашим продуктом, теперь нам надо добавить цену и названия отображаемое в самом приложении.
По цене ничего сложного просто в этом поле из дропдауна выбираем нужную и все (если надо можно добавить скидку на определенный период нажав + в этом поле)
Теперь переводы и названия нажимаем добавить локализацию
Видим такое меню
Localization - ну собственно для какого языка перевод
Display Name - это поле мы потом получим в нашем приложении как название
Description - это поле мы потом получим в нашем приложении как описание продукта
Все далее нажимаем сохранить в верхнем правом углу и готово мы создали наш продукт!
Можем повтотрить все действия и добавить нужные нам предметы!
Настройка Android
Все тоже самое что и для iOS только проще )
Заходим на https://play.google.com/console/ выбираем нужное приложение далее в правом меню выбираем продукты->продукты в приложении и нажимаем создать новый продукт.
В открывшемся окне вводим ID нового продукта, название ,описание, и устанавливаем цену.
Все! На этом со сторами мы закончили.
Настройка самого приложения
Для начала установим пакет для платежей
flutter pub add in_app_purchase
Далее создадим сервис который будет обрабатывать все наши манипуляции со стором
и добавим в него все необходимое.
class InAppPurchaseService {
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
final Stream<List<PurchaseDetails>> _storeSubscription =
InAppPurchase.instance.purchaseStream;
InAppPurchase get instance => _inAppPurchase;
}
Также нам нужно создать списки с айдиншиками наших объектов в сторе чтобы запрашивать их из стора.
static const Set<String> coins = {
'70_coins',
'350_coins',
'700_coins',
'1400_coins',
'3500_coins',
'7000_coins',
'17500_coins',
};
Далее добавим метод для получения продуктов из стора
Future<List<ProductDetails>> getProductsByType(StoreItemType type) async {
final Set<String> productsIds;
switch (type) {
case StoreItemType.coins:
productsIds = StoreProductsIds.coins;
break;
case StoreItemType.subscription:
productsIds = StoreProductsIds.subscriptions;
break;
}
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
return [];
}
final ProductDetailsResponse productDetailResponse =
await _inAppPurchase.queryProductDetails(productsIds);
if (productDetailResponse.error != null ||
productDetailResponse.productDetails.isEmpty) {
return [];
}
return productDetailResponse.productDetails;
}
После вызова этого метода мы получим список продуктов в таком объекте.
class ProductDetails {
/// Creates a new product details object with the provided details.
ProductDetails({
required this.id,
required this.title,
required this.description,
required this.price,
required this.rawPrice,
required this.currencyCode,
this.currencySymbol = '',
});
/// The identifier of the product.
///
/// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console.
final String id;
/// The title of the product.
///
/// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console.
final String title;
/// The description of the product.
///
/// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console.
final String description;
/// The price of the product, formatted with currency symbol ("$0.99").
///
/// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console.
final String price;
/// The unformatted price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform.
/// The currency unit for this value can be found in the [currencyCode] property.
/// The value always describes full units of the currency. (e.g. 2.45 in the case of $2.45)
final double rawPrice;
/// The currency code for the price of the product.
/// Based on the price specified in the App Store Connect or Sku in Google Play console based on the platform.
final String currencyCode;
/// The currency symbol for the locale, e.g. $ for US locale.
///
/// When the currency symbol cannot be determined, the ISO 4217 currency code is returned.
final String currencySymbol;
}
Так продукты получить получили, но надо теперь как-то их купить.
Тут я использую класс прослойку для хендлинга всех движений со стором. Дело вот в чем после вызова метода о покупке нам надо завершить покупку после. проверки подлинности платежа на бэке! Если мы не завершим покупку деньги вернуться покупателю через 3 дня. И транзакция будет считаться отмененной.
Для этого мы создаем класс который хэндлит все стейты платежей:
class PurchaseDetailsStreamSubscription {
final InAppPurchaseService inAppPurchaseService = Get.find<InAppPurchaseService>();
final Function()? onPending;
final Function(PurchaseDetails purchaseDetails)? onPurchased;
final Function()? onError;
final Function()? onRestored;
final Function()? onCanceled;
StreamSubscription<List<PurchaseDetails>>? _streamSubscription;
PurchaseDetailsStreamSubscription({
this.onPending,
this.onPurchased,
this.onError,
this.onRestored,
this.onCanceled,
});
Future<void> init() async {
_streamSubscription = inAppPurchaseService.getStoreSubscription().listen(
(List<PurchaseDetails> events) {
Future.forEach(
events,
(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.pendingCompletePurchase) {
await inAppPurchaseService.completePurchase(purchaseDetails);
}
switch (purchaseDetails.status) {
case PurchaseStatus.pending:
onPending?.call();
break;
case PurchaseStatus.purchased:
onPurchased?.call(purchaseDetails);
break;
case PurchaseStatus.error:
onError?.call();
break;
case PurchaseStatus.restored:
onRestored?.call();
break;
case PurchaseStatus.canceled:
onCanceled?.call();
break;
}
},
);
},
);
}
void close() {
_streamSubscription?.cancel();
}
}
Далее создаем еще два метода в нашем сервисе для покупок.
Future<bool> buyItemInStore(ProductDetails product) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
}
Future<void> completePurchase(PurchaseDetails purchaseDetails) async {
await InAppPurchase.instance.completePurchase(purchaseDetails);
}
Первый как понятно инициализирует покупку на стороне платформы, второй завершает покупку.
Далее в BloC или где вам там удобнее инициализируем наш класс прослойку и передаем в него такое
purchaseDetailsStreamSubscription = PurchaseDetailsStreamSubscription(
onCanceled: closeLoader,
onError: closeLoader,
onPurchased: (PurchaseDetails purchaseDetails) async {
closeLoader();
try {
// тут мы проверяем наш платеж на стороне сервера передав данные о платеже ,
// и если все ок завершаем покупку
final bool res = await transactionRepository
.createTransaction(purchaseDetails.verificationData.serverVerificationData);
if (res) {
await inAppPurchaseService.completePurchase(purchaseDetails);
await updateBalance();
}
} catch (_, __) {}
},
)..init();
Все можем пробовать совершить покупку просто вызвав этот метод!
inAppPurchaseService.buyItemInStore(item);
С подписками история такая же история, а вот с не расходуемыми продуктам надо вызывать метод .
buyNonConsumable
Как-то так! Всем спасибо за внимание!
P.S. если что не так, поправьте плиз в комментах!) спасибо)