import StoreKit
class StoreKitManager: NSObject, ObservableObject {
static let shared = StoreKitManager()
@Published var products: [SKProduct] = []
@Published var isPurchasing = false
private var productIDs: Set<String> = [
"com.myapp.premium.monthly",
"com.myapp.premium.yearly",
"com.myapp.coin.pack.small"
]
private var completionHandlers: [String: (Result<SKPaymentTransaction, Error>) -> Void] = [:]
override private init() {
super.init()
SKPaymentQueue.default().add(self)
}
deinit {
SKPaymentQueue.default().remove(self)
}
// MARK: - Fetch Products
func fetchProducts() {
let request = SKProductsRequest(productIdentifiers: productIDs)
request.delegate = self
request.start()
}
// MARK: - Purchase Product
func purchase(_ product: SKProduct, completion: @escaping (Result<SKPaymentTransaction, Error>) -> Void) {
guard SKPaymentQueue.canMakePayments() else {
completion(.failure(StoreError.cannotMakePayments))
return
}
completionHandlers[product.productIdentifier] = completion
isPurchasing = true
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
// MARK: - Restore Purchases
func restorePurchases(completion: @escaping (Result<Void, Error>) -> Void) {
SKPaymentQueue.default().restoreCompletedTransactions()
}
// MARK: - Check Subscription Status
func hasActiveSubscription() -> Bool {
// Check receipt for active subscription
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else {
return false
}
// In production, validate receipt with your server
// This is simplified for example
return receiptData.count > 0
}
}
// MARK: - SKProductsRequestDelegate
extension StoreKitManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async {
self.products = response.products
// Log invalid product IDs
for invalidID in response.invalidProductIdentifiers {
print("Invalid product ID: \(invalidID)")
}
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Product request failed: \(error)")
}
}
// MARK: - SKPaymentTransactionObserver
extension StoreKitManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
handlePurchased(transaction)
case .failed:
handleFailed(transaction)
case .restored:
handleRestored(transaction)
case .deferred, .purchasing:
break
@unknown default:
break
}
}
}
private func handlePurchased(_ transaction: SKPaymentTransaction) {
// Deliver content
deliverPurchase(for: transaction.payment.productIdentifier)
// Complete transaction
SKPaymentQueue.default().finishTransaction(transaction)
// Call completion handler
if let handler = completionHandlers[transaction.payment.productIdentifier] {
handler(.success(transaction))
completionHandlers.removeValue(forKey: transaction.payment.productIdentifier)
}
isPurchasing = false
}
private func handleFailed(_ transaction: SKPaymentTransaction) {
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
print("Purchase failed: \(error.localizedDescription)")
}
if let handler = completionHandlers[transaction.payment.productIdentifier] {
handler(.failure(error))
completionHandlers.removeValue(forKey: transaction.payment.productIdentifier)
}
}
SKPaymentQueue.default().finishTransaction(transaction)
isPurchasing = false
}
private func handleRestored(_ transaction: SKPaymentTransaction) {
deliverPurchase(for: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchase(for productID: String) {
// Unlock content based on product ID
UserDefaults.standard.set(true, forKey: productID)
print("Content delivered for: \(productID)")
}
}
enum StoreError: Error {
case cannotMakePayments
case noProductsFound
}
StoreKit enables selling digital goods and subscriptions within iOS apps. I request product info from App Store Connect with product identifiers, then display prices in the user's currency. Purchase flows use SKPaymentQueue to add transactions, which get validated on Apple's servers. Transaction observers handle purchase success, failure, and restoration. For subscriptions, I check receipt validation to verify active status. StoreKit 2 simplifies with async/await APIs and automatic receipt validation. App Store Server API handles server-side validation. I test purchases with sandbox accounts before production. Proper error handling addresses network issues, cancelled purchases, and pending transactions.