StoreKit 2 в iOS 26: ръководство за IAP, абонаменти и тестване
Цялостно ръководство за StoreKit 2 в iOS 26: продукти, покупки, JWS верификация, абонаменти, Win-Back Offers и локално тестване с .storekit файлове и SwiftUI код.
StoreKit 2 е модерният, базиран на Swift Concurrency API на Apple за покупки в приложението (in-app purchases) и абонаменти. Той заменя стария Objective-C ориентиран StoreKit от ерата на iOS 3. В iOS 26 рамката получи няколко важни допълнения: Win-Back Offers, по-добро интегриране със Server-Side Notifications V2 и подобрена верификация през JWS подписи. Това ръководство показва как да внедрите продукти, да валидирате транзакции, да обработвате абонаменти и да тествате всичко локално, без да напускате Xcode.
StoreKit 2 работи изцяло с async/await и AsyncSequence, което елиминира делегатния модел на StoreKit 1.
Всяка транзакция идва вече подписана с JWS (JSON Web Signature), затова клиентската верификация е задължителна стъпка.
iOS 18 въведе Win-Back Offers, които работят и в iOS 26 и позволяват автоматично възстановяване на отписали се потребители.
Локалното тестване с .storekit конфигурационен файл премахва нуждата от Sandbox акаунт за повечето сценарии.
Свойството Transaction.currentEntitlements е единственият достоверен източник за активни покупки. Никога не разчитайте на UserDefaults.
В iOS 26 минималният deployment target за нови StoreKit 2 фичи е iOS 15, а някои API-та (Win-Back) изискват iOS 18+.
Какво е StoreKit 2 и какво ново има в iOS 26?
StoreKit 2 е третото поколение на API-то за монетизация на Apple, обявено на WWDC 2021 и достъпно от iOS 15 нататък. За разлика от оригиналния StoreKit, който се базираше на SKPaymentQueue, делегати и notification центрове наследени още от Objective-C епохата, StoreKit 2 е написан изцяло на Swift и използва async/await, AsyncSequence и силно типизирани value types вместо NSDictionary базирани отговори.
Честно казано, разликата в developer experience е драматична. Първото приложение, в което мигрирах от StoreKit 1, се сви от около 600 реда delegate boilerplate до към 180 реда четим Swift. И никога не съм мечтал да се връщам обратно.
В iOS 26 рамката получи няколко допълнения. Най-важното е стабилизирането на Win-Back Offers (въведени в iOS 18), които позволяват на разработчиците да предлагат специални условия на потребители, отписали се от абонамент. Има и подобрена поддръжка за Promotional Offers с подписи, генерирани от App Store Server API, и нов Transaction.updates канал, който агресивно се рестартира при backgrounding на приложението.
Според официалната Apple Developer документация за StoreKit, минималният deployment target за основните StoreKit 2 API-та остава iOS 15, но някои нови възможности изискват iOS 17 или iOS 18.
StoreKit 1 срещу StoreKit 2: какви са разликите?
Ако вече сте работили с StoreKit 1, миграцията към StoreKit 2 не е просто toggle. Двата API-та съществуват едновременно и могат да се използват в едно и също приложение. Но за нови проекти няма причина да започвате с класическата рамка. Таблицата по-долу обобщава ключовите различия, които срещам най-често на code review.
Аспект
StoreKit 1
StoreKit 2
Език и архитектура
Objective-C, delegate базиран
Swift, async/await, AsyncSequence
Верификация на транзакции
App Store receipt (PKCS#7), нужен сървър
JWS подписани транзакции, локална проверка
Минимална версия
iOS 3+
iOS 15+
Получаване на продукти
SKProductsRequest + делегат
try await Product.products(for:)
Active entitlements
Парсване на receipt
Transaction.currentEntitlements
Тип-безопасност
Слаба (стрингови ключове)
Силна (enum-и, struct-и)
Refund handling
Само сървърно
Клиентски beginRefundRequest
Зареждане на продукти с Product API
Първата стъпка във всяка интеграция е извличането на конфигурираните в App Store Connect продукти. В StoreKit 2 това е едно извикване на Product.products(for:), което приема масив от product идентификатори и връща масив от Product структури.
import StoreKit
@MainActor
final class StoreManager: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedProductIDs: Set<String> = []
private let productIDs: Set<String> = [
"com.example.app.pro_monthly",
"com.example.app.pro_yearly",
"com.example.app.remove_ads"
]
func loadProducts() async {
do {
let storeProducts = try await Product.products(for: productIDs)
self.products = storeProducts.sorted { $0.price < $1.price }
} catch {
print("Грешка при зареждане на продукти: \(error)")
}
}
}
Обърнете внимание, че Product вече съдържа всичко нужно за UI: displayName, description, displayPrice (вече локализирана като стринг) и subscription (опционален обект с информация за период, free trial и introductory offer). Не ви трябва NumberFormatter за цената, защото Apple я форматира за валутата и локала на потребителя.
Ако строите по-сложни модели на състоянието около това, погледнете нашето ръководство за Observation Framework и @Observable в SwiftUI. Модерният заместител на ObservableObject ще ви позволи да премахнете @Published декорациите.
Реализация на покупка и обработка на резултата
Същинската покупка е едно извикване на product.purchase(). Резултатът е enum Product.PurchaseResult с три случая: .success(let verification), .userCancelled и .pending (например при Ask to Buy родителски контрол).
extension StoreManager {
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await updateEntitlements()
await transaction.finish()
case .userCancelled:
return
case .pending:
// Изчаква външно одобрение (родител, банка)
return
@unknown default:
return
}
}
}
Верификация на транзакции с JWS
Една от най-съществените промени спрямо StoreKit 1 е, че всяка транзакция идва обвита в VerificationResult<Transaction>. Apple подписва JWS токена с App Store ключ, а StoreKit го верифицира локално, без нужда от сървърно извикване. Това спестява латентност и премахва зависимостта от backend за прости приложения.
enum StoreError: Error {
case failedVerification
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw StoreError.failedVerification
case .verified(let safe):
return safe
}
}
За production приложения с реален backend все пак препоръчвам да изпращате jwsRepresentation на сървъра за двойна проверка, защото клиентската верификация може да бъде заобиколена при jailbreak. Подробностите ги има в App Store Server API документацията.
Слушане за външни транзакции
Покупки, направени извън вашия flow (например family sharing, повторни покупки или resolved pending transactions) пристигат през Transaction.updates. Това е AsyncSequence, която трябва да започне да слуша възможно най-рано в жизнения цикъл на приложението.
func observeTransactionUpdates() -> Task<Void, Never> {
Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
await self.updateEntitlements()
await transaction.finish()
} catch {
print("Невалидна транзакция: \(error)")
}
}
}
}
Стартирайте този Task от App.init() или ранен .task модификатор. Ако се чудите как AsyncSequence се вписва в общата картина на конкурентността, прочетете нашето ръководство за Swift Concurrency с async/await и актьори.
Абонаменти, групи и Win-Back Offers
Абонаментите в StoreKit 2 се организират в Subscription Groups. Потребител може да има активен само един продукт от дадена група в даден момент. Това е критично за дизайна на ценовия план: monthly и yearly tier на един и същ продукт винаги отиват в една група.
Получаването на статуса на абонамент става през Product.subscription?.status:
Стойността .state на статуса включва .subscribed, .expired, .inBillingRetryPeriod, .inGracePeriod и .revoked. UI логиката трябва да третира grace period и billing retry като активен абонамент, защото потребителят все още има достъп, докато App Store опитва да възстанови плащането.
Win-Back Offers (iOS 18+)
Win-Back Offers са специални промоционални оферти, които App Store предлага автоматично на потребители, отказали се от абонамент. Конфигурират се в App Store Connect и стават достъпни през Product.SubscriptionInfo.WinBackOffer:
if #available(iOS 18.0, *) {
let offers = product.subscription?.winBackOffers ?? []
if let offer = offers.first {
let result = try await product.purchase(options: [.winBackOffer(offer)])
// обработка...
}
}
Тестване на StoreKit 2 в Xcode
Една от най-подценяваните възможности на StoreKit 2 е локалното тестване с .storekit конфигурационен файл. Това премахва нуждата да създавате Sandbox tester акаунти за всеки сценарий и работи дори в Simulator. Честно казано, спестих си часове чакане на Sandbox sync, откакто започнах да го използвам по подразбиране.
В Xcode: File → New → File → StoreKit Configuration File.
Дефинирайте продукти и абонаментни групи в редактора.
В схемата за изпълнение: Edit Scheme → Run → Options → StoreKit Configuration, после изберете файла.
Стартирайте. Приложението вижда продуктите дори без App Store Connect конфигурация.
За автоматизирани тестове Apple предоставят SKTestSession, която ви позволява да манипулирате транзакции от XCTest или Swift Testing:
За production системи с backend, App Store Server Notifications V2 са незаменими. Apple праща JWS-подписан webhook при всяко събитие: нов абонамент, подновяване, refund, отказ. За разлика от V1, форматът е стабилен и включва пълни Transaction и RenewalInfo payload-и.
Конфигурирането става в App Store Connect (App Information → App Store Server Notifications). Един често пропускан детайл: задайте отделни URL-и за Production и Sandbox, защото и двата ще получават събития от съответната среда. Вижте официалната спецификация за Server Notifications V2 за пълния списък типове събития. За schema validation на JWS payload-ите силно препоръчвам RFC 7515 като референция.
Чести грешки и как да ги избегнете
След няколко години код-ревюта на StoreKit 2 интеграции, ето грешките, които виждам най-често:
Кеширане на entitlements в UserDefaults. Просто никога. Винаги питайте Transaction.currentEntitlements, защото той е достоверният източник и работи правилно при family sharing и device switch.
Забравяне на transaction.finish(). Резултат: безкрайно пристигащи покупки през Transaction.updates при всеки старт.
Стартиране на Transaction.updates в SwiftUI .onAppear. Твърде късно. Транзакции, пристигнали преди изобразяването на view, се губят. Стартирайте в App.init или .task.
Пренебрегване на .pending резултата. Ask to Buy транзакциите изискват UI обратна връзка. Иначе потребителят остава с впечатление, че плащането е минало.
Тестване само в Sandbox..storekit файлът е по-бърз и покрива 95% от сценариите без зависимост от Apple infrastructure.
Често задавани въпроси
Каква е разликата между StoreKit 1 и StoreKit 2?
StoreKit 2 е написан на Swift с async/await и силно типизирани структури, докато StoreKit 1 използва Objective-C делегати и слабо типизирани NSDictionary отговори. StoreKit 2 също предоставя локална JWS верификация на транзакции, което премахва нуждата от сървърна валидация за прости приложения.
Мога ли да използвам StoreKit 2 в проект, който поддържа iOS 14?
Не директно. StoreKit 2 изисква минимум iOS 15. Можете обаче да го използвате с #available(iOS 15.0, *) блок и да оставите StoreKit 1 като fallback за по-стари версии.
Как се тества StoreKit 2 без App Store Connect акаунт?
Чрез .storekit конфигурационен файл, създаден в Xcode (File → New → File → StoreKit Configuration File). След като го изберете в Run схемата, приложението вижда дефинираните локално продукти и може да симулира покупки, refund-и и subscription renewals без интернет връзка.
Как се верифицира JWS подпис на транзакция?
StoreKit 2 верифицира JWS подписа автоматично и връща VerificationResult.verified или .unverified. За допълнителна сигурност в production среда изпратете transaction.jwsRepresentation към вашия сървър, който може да направи независима проверка през App Store Server API.
Какво представляват Win-Back Offers в iOS 26?
Win-Back Offers са промоционални оферти, които App Store автоматично предлага на потребители, отписали се от абонамент. Въведени са в iOS 18 и остават напълно поддържани в iOS 26. Конфигурират се в App Store Connect и се достъпват през product.subscription?.winBackOffers.
Практически гайд за SwiftUI accessibility в iOS 26: как да добавиш VoiceOver, Dynamic Type, custom Rotor, Reduce Motion и да преминеш App Store audit-а.
Swift Testing с @Test, #expect, параметризирани тестове и паралелно изпълнение по подразбиране. Пълен преход от XCTest в Swift 6 и Xcode 16 с реални примери, режимите на оперативна съвместимост от WWDC26 и капани, които да избегнете.
Apple представи Interactive Snippets в iOS 26 — най-голямата стъпка напред за App Intents досега. Разгледайте новия SnippetIntent протокол, метода reload(), верижните потвърждения и пълен SwiftUI пример с интерактивен брояч — всичко нужно, за да изнесете функциите си извън приложението през Spotlight, Siri и Shortcuts.