What’s new in Apphud: Refund Requests Win Back Solution, Web-to-App Match Quality, Integration Improvements, and SDK UpdatesLet’s see
Apphud
Why Apphud?PricingContact
Ren
Ren
June 11, 2021
1 min read

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

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

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

StoreKit 2

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

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

Продукты

Продукты в StoreKit 2Продукты в StoreKit 2

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 – набор транзакций, которые отражают состояние всех приобретенных подписок и разовых покупок.

История транзакций в StoreKit 2История транзакций в StoreKit 2

Например, если в вашем приложении есть и подписка, и 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 APIStoreKit 2 API

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

StoreKit 2 + Apphud = ❤️

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

  • SDK (наконец-то) избавится от передачи огромных чеков на сервер, что уменьшит исходящий трафик с устройства.
  • Чем проще API, тем меньше багов в SDK и обработке транзакций.
  •  iOS возьмет на себя ответственность за обновление подписок, что позволит Apphud сконцентрироваться на более важных функциях сервиса.
Ren
Ren
Co-founder at Apphud
Ex iOS app and game developer. 11 years in the industry since iOS 3. More than 50 apps are in the background with 4 exits. Entrepreneur and traveler.

Related Posts