> Работали ли с подписками и покупками через StoreKit или другие сервисы (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: КРЕЙТЕКС

Стек: iOS, Swift

> Пример ответа

Короткий ответ

Да, работал с StoreKit 2 и StoreKit 1: оформление подписок (auto-renewable), non-consumable покупок, восстановление транзакций, обработка ошибок (например, StoreKitError, .pending, .failed), проверка чека на сервере через App Store Server API, а также с SKProductsRequest для загрузки продуктов. Использовал Transaction.updates для обработки изменений статуса подписки в реальном времени и AppTransaction для валидации. Также есть опыт с RevenueCat для упрощения работы с подписками и аналитикой.

Подробное объяснение

StoreKit 2 - это современный API, который сильно упрощает работу по сравнению с StoreKit 1. Основные сущности: Product, Transaction, Entitlement, AppStore. Ключевое отличие - асинхронная модель на Swift Concurrency: Product.products(for:), product.purchase(), Transaction.currentEntitlements и Transaction.updates (поток изменений транзакций). В StoreKit 1 приходилось вручную реализовывать SKPaymentTransactionObserver, обрабатывать SKPaymentTransactionState, а также следить за SKPaymentQueue.

Для подписок важно понимать:

  • Product.SubscriptionInfo - период, группу, уровень (tier), isFamilyShareable.
  • Transaction.revocationDate - для отмены подписки.
  • Transaction.expirationDate - для проверки активного статуса.
  • Transaction.ownershipType - .purchased или .familyShared.

Валидация чека: на клиенте можно использовать AppTransaction.shared и Transaction.latest(for:), но для критичных сценариев (например, premium-доступ) лучше проверять на сервере через App Store Server API (endpoint /verifyReceipt устарел, теперь используется App Store Server API с JWT-авторизацией).

Также стоит упомянуть StoreKit Configuration File для локального тестирования и StoreKitTest framework для UI-тестов покупок.

На практике

В реальном проекте обычно:

  1. Создаётся сервис-обёртка (например, StoreManager), который инкапсулирует всю логику StoreKit.
  2. На старте приложения подписываемся на Transaction.updates - это важно для обработки покупок, сделанных на других устройствах, и для восстановления.
  3. Проверяем Transaction.currentEntitlements - это источник прав (entitlements) для текущего пользователя.
  4. Для каждой покупки вызываем product.purchase(), обрабатываем результат: .success(let verification), .userCancelled, .pending.
  5. После успешной покупки обязательно завершаем транзакцию через await transaction.finish().
  6. Для подписок - слушаем Transaction.updates и обновляем UI при изменении статуса (например, при истечении или продлении).

Также важно учитывать:

  • Песочница (sandbox) и TestFlight имеют разные правила для подписок (например, скорость продления).
  • StoreKit 2 не поддерживает SKPaymentQueue напрямую, но можно использовать Transaction.updates для совместимости.
  • Для аналитики и A/B-тестов часто используют RevenueCat или Adapty - они берут на себя управление транзакциями и дают вебхуки.

Пример кода

SWIFT
import StoreKit
final class StoreManager {
static let shared = StoreManager()
private var updatesTask: Task<Void, Never>?
private(set) var products: [Product] = []
private init() {
updatesTask = listenForTransactions()
}
func loadProducts() async throws {
let productIDs = Set(["com.example.subscription.monthly", "com.example.premium"])
products = try await Product.products(for: productIDs)
}
func purchase(_ product: Product) async throws -> Bool {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish()
return true
case .userCancelled:
return false
case .pending:
// например, ожидание подтверждения родителя (Ask to Buy)
return false
@unknown default:
return false
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw StoreError.failedVerification
case .verified(let safe):
return safe
}
}
func isSubscriptionActive() async -> Bool {
guard let product = products.first(where: { $0.type == .autoRenewable }) else {
return false
}
guard let status = try? await product.subscription?.status.first else {
return false
}
switch status.state {
case .subscribed, .inGracePeriod:
return true
default:
return false
}
}
private func listenForTransactions() -> Task<Void, Never> {
Task.detached { [weak self] in
for await update in Transaction.updates {
do {
let transaction = try self?.checkVerified(update)
// обновляем UI, отправляем на сервер
await transaction?.finish()
} catch {
// логируем ошибку
}
}
}
}
enum StoreError: Error {
case failedVerification
}
}

Как отвечать на собеседовании

Начните с того, что у вас есть практический опыт с StoreKit 2, и упомяните, что знаете отличия от StoreKit 1. Расскажите про конкретный кейс: например, как реализовывали подписку с пробным периодом, как обрабатывали восстановление покупок, как решали проблему с истекшими подписками. Подчеркните, что понимаете важность серверной валидации и умеете работать с App Store Server API. Если использовали RevenueCat - скажите, что понимаете, зачем он нужен (упрощение, аналитика, кросс-платформа), но также умеете работать с чистым StoreKit.

Что проверяет интервьюер

  • Понимание жизненного цикла транзакции: от покупки до finish().
  • Знание механизма восстановления покупок и Transaction.updates.
  • Понимание разницы между currentEntitlements и Transaction.latest.
  • Умение обрабатывать ошибки и edge cases: pending, userCancelled, unverified.
  • Знание серверной валидации и почему она важна.
  • Понимание типов продуктов: consumable, non-consumable, auto-renewable subscription, non-renewing subscription.

Типичные ошибки

  • Не вызывать transaction.finish() - это приводит к повторным уведомлениям и проблемам с App Store.
  • Полагаться только на клиентскую проверку чека без серверной валидации.
  • Игнорировать Transaction.updates - тогда не обрабатываются покупки с других устройств и изменения статуса подписки.
  • Путать currentEntitlements (текущие права) с Transaction.latest (последняя транзакция по продукту).
  • Не обрабатывать случай .pending (например, Ask to Buy) - пользователь думает, что покупка не прошла, а она ещё в обработке.
  • Использовать устаревший verifyReceipt вместо App Store Server API.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью