Как добавить покупки внутри приложения (In-App Purchase) в iOS на 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)
}
}
}