Это совершенно новый фреймворк, написанный на чистом Swift с использованием нового синтаксиса await/async
. Главные особенности:
SKPaymentTransactionObserver
. Теперь процесс покупки происходит с помощью await
. Product
и Transaction
теперь структуры вместо классов.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 группы подписки в качестве параметра.
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()
Результатом покупки будет enum
PurchaseResult
:
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) } }
Вместо 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
.
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>?
Одной из главных особенностей нового StoreKit 2 API является улучшенная работа с подписками. Теперь получать важную информацию о подписке можно прямо из приложения!
Что доступно:
subscribed
, expired
, inBillingRetryPeriod
, inGracePeriod
, revoked
.autoRenewalStatus
, gracePeriodExpirationDate
. Что соответствует хешу pending_renewal_info
из текущего verifyReceipt
эндпоинта.Все транзакции доступны сразу после скачивания приложения и синхронизируются автоматически на каждом устройстве. Таким образом, несмотря на то, что в StoreKit 2 присутствует метод sync()
, он будет использоваться крайне редко. Можно сказать, что кнопка Restore Purchases
станет неактуальна и можно будет спрятать ее глубоко в UI – покупки восстанавливаются автоматически.
StoreKit 2 безусловно заметно упрощает жизнь разработчикам приложений для определения статуса подписки, что положительно влияет на Apphud.