Avatar ·

Как добавить покупки внутри приложения (In-App Purchase) в iOS на Swift?

Всем привет! Я начинающий iOS-разработчик, пишу своё первое приложение на Swift. Хочу добавить возможность покупки премиум-функций через In-App Purchase. Прочитал документацию Apple, но всё равно запутался. У меня есть простой экран с кнопкой "Купить Premium". Я создал продукт в App Store Connect, но не понимаю, как правильно запрашивать продукты из StoreKit и обрабатывать покупку. Вот мой текущий код (набросок): ```swift import StoreKit class PurchaseManager { func buyPremium() { // Как тут запросить продукт? // Я пробовал SKProductsRequest, но не знаю, куда вставить идентификатор продукта } func handlePurchase() { // А что делать после того, как пользователь нажал "Купить"? } } ``` Основные вопросы: 1. Как правильно получить список продуктов из App Store? 2. Как обработать успешную покупку и разблокировать функции? 3. Как проверить, купил ли пользователь продукт ранее? 4. Нужно ли использовать Observer для обработки транзакций? Буду благодарен за подробное объяснение с рабочими примерами на Swift. Заранее спасибо! <<>>

Пошаговое руководство по внедрению In-App Purchase в iOS приложении

Привет! Отличный вопрос. In-App Purchase — это обязательный навык для коммерческих приложений. Давай разберём всё по шагам, начиная от настройки в App Store Connect до кода на Swift.

1. Настройка в App Store Connect

Прежде чем писать код, убедись, что:

  • У тебя есть аккаунт разработчика Apple ($99/год).
  • В App Store Connect создан продукт (например, с идентификатором com.example.premium).
  • Продукт имеет статус "Ready to Submit" (даже если приложение ещё не опубликовано).
  • В Xcode в настройках проекта (Signing & Capabilities) добавлен In-App Purchase.

2. Базовая структура PurchaseManager

Создадим класс, который будет управлять всеми покупками:

import StoreKit

class PurchaseManager: NSObject {
    static let shared = PurchaseManager()
    private override init() {}
    
    // Идентификатор продукта из App Store Connect
    private let productID = "com.example.premium"
    
    // Хранилище для полученных продуктов
    private var products: [SKProduct] = []
    
    // Обработчик результата
    var onPurchaseComplete: ((Bool) -> Void)?
    
    // MARK: - Запрос продуктов
    func fetchProducts() {
        let request = SKProductsRequest(productIdentifiers: [productID])
        request.delegate = self
        request.start()
    }
    
    // MARK: - Покупка
    func buyProduct() {
        guard let product = products.first else {
            print("Продукты не загружены")
            return
        }
        
        if SKPaymentQueue.canMakePayments() {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
        } else {
            print("Покупки запрещены на устройстве")
        }
    }
    
    // MARK: - Восстановление покупок
    func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
}

// MARK: - SKProductsRequestDelegate
extension PurchaseManager: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        products = response.products
        
        if products.isEmpty {
            print("Продукты не найдены. Проверь идентификатор и статус в App Store Connect")
        } else {
            for product in products {
                let priceString = product.priceFormatter()?.string(from: product.price) ?? "\(product.price)"
                print("Найден продукт: \(product.localizedTitle) - \(priceString)")
            }
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Ошибка запроса продуктов: \(error.localizedDescription)")
    }
}

// MARK: - SKPaymentTransactionObserver
extension PurchaseManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchasing:
                // Покупка в процессе
                break
                
            case .purchased:
                // Успешная покупка
                completeTransaction(transaction)
                
            case .failed:
                // Ошибка покупки
                failedTransaction(transaction)
                
            case .restored:
                // Восстановленная покупка
                restoreTransaction(transaction)
                
            case .deferred:
                // Ожидание (например, родительский контроль)
                break
                
            @unknown default:
                break
            }
        }
    }
    
    private func completeTransaction(_ transaction: SKPaymentTransaction) {
        // Разблокируем функции приложения
        unlockPremium()
        
        // Завершаем транзакцию
        SKPaymentQueue.default().finishTransaction(transaction)
        onPurchaseComplete?(true)
    }
    
    private func failedTransaction(_ transaction: SKPaymentTransaction) {
        if let error = transaction.error as? SKError {
            switch error.code {
            case .paymentCancelled:
                print("Пользователь отменил покупку")
            default:
                print("Ошибка: \(error.localizedDescription)")
            }
        }
        SKPaymentQueue.default().finishTransaction(transaction)
        onPurchaseComplete?(false)
    }
    
    private func restoreTransaction(_ transaction: SKPaymentTransaction) {
        unlockPremium()
        SKPaymentQueue.default().finishTransaction(transaction)
        onPurchaseComplete?(true)
    }
    
    // MARK: - Логика разблокировки
    private func unlockPremium() {
        UserDefaults.standard.set(true, forKey: "isPremium")
        // Или сохрани в Keychain для большей безопасности
        NotificationCenter.default.post(name: .premiumUnlocked, object: nil)
    }
}

// MARK: - Вспомогательные расширения
extension SKProduct {
    func priceFormatter() -> NumberFormatter? {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = self.priceLocale
        return formatter
    }
}

extension Notification.Name {
    static let premiumUnlocked = Notification.Name("premiumUnlocked")
}

3. Использование в приложении

В AppDelegate или SceneDelegate добавь наблюдателя транзакций:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Добавляем наблюдателя
    SKPaymentQueue.default().add(PurchaseManager.shared)
    
    // Сразу запрашиваем продукты
    PurchaseManager.shared.fetchProducts()
    return true
}

В контроллере с кнопкой покупки:

@IBAction func buyButtonTapped(_ sender: UIButton) {
    PurchaseManager.shared.onPurchaseComplete = { [weak self] success in
        DispatchQueue.main.async {
            if success {
                self?.showPremiumUnlockedAlert()
            } else {
                self?.showErrorAlert()
            }
        }
    }
    PurchaseManager.shared.buyProduct()
}

4. Проверка статуса покупки

Чтобы проверить, купил ли пользователь премиум:

func isPremiumUser() -> Bool {
    return UserDefaults.standard.bool(forKey: "isPremium")
}

5. Важные моменты

  • Тестирование: Используй Sandbox-аккаунты (создаются в App Store Connect). На симуляторе покупки не работают!
  • Безопасность: Храни статус премиума в Keychain или серверной базе данных. UserDefaults можно взломать.
  • Восстановление покупок: Всегда добавляй кнопку "Восстановить покупки" (Restore Purchases).
  • Обработка ошибок: Всегда проверяй SKPaymentQueue.canMakePayments().

Этот код — минимальная рабочая основа. Для продакшена добавь обработку очереди транзакций, работу с сервером для верификации receipt и более надёжное хранение статуса.

<<>>

Альтернативный подход: Использование SwiftyStoreKit (библиотека-обёртка)

Если хочешь упростить интеграцию и избежать написания boilerplate-кода, рекомендую использовать популярную библиотеку SwiftyStoreKit (устанавливается через CocoaPods или SPM). Она значительно сокращает код и обрабатывает множество edge-кейсов.

Пример с SwiftyStoreKit:

import SwiftyStoreKit

class PurchaseManager {
    static let shared = PurchaseManager()
    private let productID = "com.example.premium"
    
    // Запрос информации о продукте
    func fetchProduct(completion: @escaping (SKProduct?) -> Void) {
        SwiftyStoreKit.retrieveProductsInfo([productID]) { result in
            if let product = result.retrievedProducts.first {
                completion(product)
            } else if let error = result.error {
                print("Ошибка: \(error)")
                completion(nil)
            }
        }
    }
    
    // Покупка
    func purchase(completion: @escaping (Bool) -> Void) {
        SwiftyStoreKit.purchaseProduct(productID) { result in
            switch result {
            case .success(let details):
                print("Покупка успешна: \(details.productId)")
                self.unlockPremium()
                completion(true)
            case .error(let error):
                print("Ошибка: \(error)")
                completion(false)
            }
        }
    }