StoreKit 2 превратил встроенные покупки из лабиринта делегатов в несколько строк асинхронного Swift. Вот схема, которую мы используем в своих приложениях: один небольшой класс Store, пейвол и несколько правил, которые устраивают App Review.
1. Создайте продукты
В App Store Connect создайте группу подписок и добавьте планы, например месячный и годовой. Запишите идентификаторы продуктов. Для локального тестирования добавьте в Xcode StoreKit Configuration File с теми же идентификаторами и выберите его в схеме. Тогда в симуляторе можно покупать, продлевать и отменять подписку без sandbox-аккаунта.
2. Один Store на всё приложение
import StoreKit
@MainActor
final class Store: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var isPro = false
private let ids = ["com.example.pro.monthly", "com.example.pro.yearly"]
private var updates: Task<Void, Never>?
init() {
// Renewals, refunds and purchases made on other devices arrive here.
updates = Task { [weak self] in
for await result in Transaction.updates {
await self?.handle(result)
}
}
Task {
await loadProducts()
await refreshEntitlements()
}
}
func loadProducts() async {
do {
products = try await Product.products(for: ids).sorted { $0.price < $1.price }
} catch {
print("Could not load products: \(error)")
}
}
func buy(_ product: Product) async throws {
switch try await product.purchase() {
case .success(let result):
await handle(result)
case .userCancelled, .pending:
break
@unknown default:
break
}
}
func restore() async {
try? await AppStore.sync()
await refreshEntitlements()
}
func refreshEntitlements() async {
var active = false
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result, transaction.revocationDate == nil {
active = true
}
}
isPro = active
}
private func handle(_ result: VerificationResult<Transaction>) async {
guard case .verified(let transaction) = result else { return }
await transaction.finish()
await refreshEntitlements()
}
}
Создайте его один раз, например как @StateObject в вашем App, и передавайте вниз через .environmentObject. Начинайте слушать Transaction.updates как можно раньше, иначе покупки, завершённые вне приложения, можно пропустить.
3. Почему важна проверка
Каждая транзакция приходит в обёртке VerificationResult. StoreKit проверяет подпись Apple на устройстве, поэтому .verified означает, что покупка настоящая. Открывайте контент только для проверенных транзакций и всегда вызывайте finish() после выдачи.
4. Пейвол в несколько строк
В iOS 17 и новее SubscriptionStoreView даёт готовый пейвол, удобный для App Review:
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
VStack(spacing: 8) {
Text("Go Pro").font(.largeTitle.bold())
Text("Unlimited exports and every template.")
}
}
.storeButton(.visible, for: .restorePurchases)
Если делаете свой, показывайте цену и период каждого плана через product.displayPrice, добавьте ссылки на условия использования и конфиденциальность и кнопку восстановления.
5. Пробные периоды и вводные предложения
Вводные предложения настраиваются в App Store Connect. Чтобы показывать «Начать бесплатный период» только тем, кто может им воспользоваться, сначала проверьте право:
let eligible = await product.subscription?.isEligibleForIntroOffer ?? false
6. Что App Review проверяет в пейволе
- Цена и период оплаты понятны до покупки
- Длительность бесплатного периода и что будет после
- Работающая кнопка восстановления покупок
- Ссылки на условия использования и политику конфиденциальности
- Никаких тёмных паттернов вроде спрятанной кнопки закрытия
Остальное — в нашем списке самых частых отказов в App Store.
7. Серверная часть, когда она нужна
Для приложения на одном устройстве хватает прав доступа на устройстве. Если пользователи входят на нескольких платформах или вам нужны данные о доходе, добавьте в бэкенд App Store Server Notifications V2, чтобы сервер сразу узнавал о продлениях и возвратах.
Частые вопросы
Нужна ли проверка чеков со StoreKit 2?
Не на устройстве: StoreKit 2 сам проверяет подписанные транзакции. Сервер нужен, только если вы делите доступ между платформами или аккаунтами.
Как тестировать подписки без оплаты?
Для локальных тестов используйте StoreKit Configuration File в Xcode, для сквозных — sandbox-аккаунт или TestFlight. В sandbox продления идут по ускоренному графику.
Какая минимальная версия iOS для StoreKit 2?
StoreKit 2 работает на iOS 15 и новее. SubscriptionStoreView и другим витринам нужна iOS 17.
Сколько Apple берёт с подписок?
По умолчанию 30 %, 15 % для участников Small Business Program и 15 % с подписки после первого года подписчика.