Привет, меня зовут Алексей. Сегодня я расскажу про внедрение внутренних покупок в мобильное приложение на 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. если что не так, поправьте плиз в комментах!) спасибо)

Комментарии (0)