Apphud – integrate, analyze and improve in-app purchases and subscriptions in your iOS/Android apps

Разработка

StoreKit 2: что это такое и как он повлияет на Apphud?

Всем привет!

На WWDC 2021 произошли некоторые крупные изменения, касающиеся оформления встроенных покупок и валидации подписок. А именно Apple представила StoreKit 2.

StoreKit 2

Это совершенно новый фреймворк, написанный на чистом Swift с использованием нового синтаксиса await/async. Главные особенности:

  • Убрали App Store binary receipt. Да, длинные base64 строки теперь не нужны и спрятаны под капотом. Вместо этого Apple предлагает разработчикам запрашивать с сервера информацию о транзакциях с помощью нового App Store Server API
  • Убрали SKPaymentTransactionObserver. Теперь процесс покупки происходит с помощью await
  • Убрали SKRequestDelegate. Запрос информации о продуктах также происходит с помощью await
  • Добавили множество API, например, управление возвратами из приложения
  • Добавили проверку статуса подписки и всю основную информацию о подписке
  • Можно получать список исторических транзакций и последнюю транзакцию
  • Транзакции теперь автоматически валидируются при покупке
  • Product и Transaction теперь структуры вместо классов
  • Каждая транзакция шифруется с использованием JWS (JSON Web Signature)

Продукты

Product стал структурой и запрос на получение данных по product id теперь осуществляется с помощью static метода:

public static func request(with identifiers: Set<String>) async throws -> [Product]

На практике это выглядит так:

func loadProducts() async {
do {
let ids = [ "com.apphud.weekly", "com.apphud.weekly2", "com.apphud.monthly"]
self.products = try await Product.request(with: Set(ids))
} catch {
print("error while loading products: \(error.localizedDescription)")
}
}

Все намного проще, не правда ли?

Так же у Product появились новые методы, например, структура ProductType, которая возвращает тип встроенной покупки:

public static var consumable: Product.ProductType
public static var nonConsumable: Product.ProductType
public static var nonRenewable: Product.ProductType
public static var autoRenewable: Product.ProductType

Или, например, отформатированная цена в виде строки:

/// A localized string representation of `price`.
public let displayPrice: String

Еще одним крутым нововведением являются методы проверки доступности триала:

/// Whether the user is eligible to have an introductory offer applied to their purchase.
public var isEligibleForIntroOffer: Bool { get async }
public static func isEligibleForIntroOffer(for groupID: String) async -> Bool

Первый getter метод проверяет конкретно данный Product, а второй – static метод, принимающий ID группы подписки в качестве параметра.

App Account Token

Apple добавила анонимный идентификатор пользователя к Transaction, который остается навсегда и доступен, в том числе в REST API. Это означает, что теперь можно будет однозначно сопоставлять пользователя приложения с данными по транзакциям из REST API.

/// A UUID that associates the purchase with an account in your system.
public static func appAccountToken(_ token: UUID) -> Product.PurchaseOption

Покупка

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

let result = try await product.purchase()

Результатом покупки будет enumPurchaseResult:

public enum PurchaseResult {
/// The purchase succeeded with a `Transaction`.
case success(VerificationResult<Transaction>)

/// The user cancelled the purchase.
case userCancelled

/// The purchase is pending some user action.
///
/// These purchases may succeed in the future, and the resulting `Transaction` will be
/// delivered via `Transaction.listener`
case pending
}

А если произошла какая-то ошибка, то она вернется в catch.

Ниже приведен пример использования метода покупки:

func purchase(_ product: Product) async throws -> Transaction? {
       //Begin a purchase.
       let result = try await product.purchase()

       switch result {
       case .success(let verification):
           let transaction = try checkVerified(verification)

           //Deliver content to the user.
           await updatePurchasedIdentifiers(transaction)

           //Always finish a transaction.
           await transaction.finish()

           return transaction
       case .userCancelled, .pending:
           return nil
       default:
           return nil
       }
   }

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
       //Check if the transaction passes StoreKit verification.
       switch result {
       case .unverified:
           //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
           throw StoreError.failedVerification
       case .verified(let safe):
           //If the transaction is verified, unwrap and return it.
           return safe
       }
}

Чтобы запустить асинхронный код внутри синхронного, достаточно обернуть его в async {}:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let product = products[indexPath.row]
async {
await self.purchaseAsync(product: product)
}
}

TransactionListener

Вместо Transaction Observer Apple добавили свой Transaction Listener. Он отдает транзакции, которые не пришли в прямом вызове метода purchase().

// Start a transaction listener as close to app launch as possible so you don't miss any transactions.

taskHandle = listenForTransactions()

func listenForTransactions() -> Task.Handle<Void, Error> {
       return detach {
           //Iterate through any transactions which didn't come from a direct call to `purchase()`.
           for await result in Transaction.listener {
               do {
                   let transaction = try self.checkVerified(result)

                   //Deliver content to the user.
                   await self.updatePurchasedIdentifiers(transaction)

                   //Always finish a transaction.
                   await transaction.finish()
               } catch {
                   //StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
                   print("Transaction failed verification")
               }
           }
       }
   }

Transaction Listener может использоваться для случаев “Ask To Buy” или “Strong Customer Authentication”, когда транзакция переходит в состояние pending.

Transaction History

StoreKit 2 также может возвращать историю транзакций в структуре TransactionSequence. Можно получить все транзакции, либо только последнюю, либо получить Entitlements – набор транзакций, которые отражают состояние всех приобретенных подписок и разовых покупок.

Например, если в вашем приложении есть и подписка, и non-consumable покупка, и пользователь приобрел оба продукта, то currentEntitlements вернет последнюю активную транзакцию для подписки и транзакцию для non-consumable покупки.

/// A sequence of every transaction for this user and app.
public static var all: Transaction.TransactionSequence { get }

/// Returns all transactions for products the user is currently entitled to
///
/// i.e. all currently-subscribed transactions, and all purchased (and not refunded) non-consumables
public static var currentEntitlements: Transaction.TransactionSequence { get }

/// Get the transaction that entitles the user to a product.
/// - Parameter productID: Identifies the product to check entitlements for.
/// - Returns: A transaction if the user is entitled to the product, or `nil` if they are not.
public static func currentEntitlement(for productID: String) async -> VerificationResult<Transaction>?

/// The user's latest transaction for a product.
/// - Parameter productID: Identifies the product to check entitlements for.
/// - Returns: A verified transaction, or `nil` if the user has never purchased this product.
public static func latest(for productID: String) async -> VerificationResult<Transaction>?

Subscription Info

Одной из главных особенностей нового StoreKit 2 API является улучшенная работа с подписками. Теперь получать важную информацию о подписке можно прямо из приложения!

Что доступно:

  • Renewal State. Позволяет узнать общий статус подписки: subscribed, expired, inBillingRetryPeriod, inGracePeriod, revoked.
  • Renewal Info. Отдает общую информацию о подписке, например, autoRenewalStatus, gracePeriodExpirationDate. Что соответствует хешу pending_renewal_info из текущего verifyReceipt эндпоинта.
  • Latest transaction. Отдает последнюю активную транзакцию.

Все транзакции доступны сразу после скачивания приложения и синхронизируются автоматически на каждом устройстве. Таким образом, несмотря на то, что в StoreKit 2 присутствует метод sync(), он будет использоваться крайне редко. Можно сказать, что кнопка Restore Purchases станет неактуальна и можно будет спрятать ее глубоко в UI – покупки восстанавливаются автоматически.

StoreKit 2 + Apphud = ❤️

StoreKit 2 безусловно заметно упрощает жизнь разработчикам приложений для определения статуса подписки, что положительно влияет на Apphud.

  • SDK (наконец-то) избавится от передачи огромных чеков на сервер, что уменьшит исходящий трафик с устройства.
  • Чем проще API, тем меньше багов в SDK и обработке транзакций.
  • iOS возьмет на себя ответственность за обновление подписок, что позволит Apphud сконцентрироваться на более важных функциях сервиса.

К сожалению, StoreKit 2 доступен только для устройств iOS 15 и выше. Может пройти еще несколько лет, прежде чем новый фреймворк станет основным.

Для предыдущих версий iOS по-прежнему необходимо использовать старый API. Однако мы обновим наш SDK таким образом, чтобы он поддерживал как старые версии iOS, так и iOS 15 с использованием StoreKit 2. Для устройств на iOS 15 в Apphud SDK появятся новые методы из StoreKit 2 с использованием await/async.

Обновленный Apphud SDK с поддержкой StoreKit 2 ожидайте позднее этим летом.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *