/// Copyright (c) 2018 Razeware LLC /// /// Permission is hereby granted, free of charge, to any person obtaining a copy /// of this software and associated documentation files (the "Software"), to deal /// in the Software without restriction, including without limitation the rights /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell /// copies of the Software, and to permit persons to whom the Software is /// furnished to do so, subject to the following conditions: /// /// The above copyright notice and this permission notice shall be included in /// all copies or substantial portions of the Software. /// /// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, /// distribute, sublicense, create a derivative work, and/or sell copies of the /// Software in any work that is designed, intended, or marketed for pedagogical or /// instructional purposes related to programming, coding, application development, /// or information technology. Permission for such use, copying, modification, /// merger, publication, distribution, sublicensing, creation of derivative works, /// or sale is expressly withheld. /// /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN /// THE SOFTWARE. import StoreKit public typealias ProductIdentifier = String public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void extension Notification.Name { static let IAPHelperPurchaseNotification = Notification.Name("IAPHelperPurchaseNotification") static let IAPHelperPurchaseNotificationFail = Notification.Name("IAPHelperPurchaseNotificationFail") } open class IAPHelper: NSObject { private let productIdentifiers: Set private var purchasedProductIdentifiers: Set = [] private var productsRequest: SKProductsRequest? private var productsRequestCompletionHandler: ProductsRequestCompletionHandler? let receiptURL = Bundle.main.appStoreReceiptURL // MARK: - Init public init(productIds: Set) { productIdentifiers = productIds super.init() loadPurchase() SKPaymentQueue.default().add(self) } // MARK: - Load purchased func loadPurchase() { purchasedProductIdentifiers.removeAll() productIdentifiers.forEach { (productIdentifier) in let purchased = UserDefaults.standard.bool(forKey: productIdentifier) if purchased { purchasedProductIdentifiers.insert(productIdentifier) } } } } // MARK: - StoreKit API extension IAPHelper { public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) { productsRequest?.cancel() productsRequestCompletionHandler = completionHandler productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) productsRequest!.delegate = self productsRequest!.start() } public func buyProduct(_ product: SKProduct) { print("Buying \(product.productIdentifier)...") let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) } public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool { return purchasedProductIdentifiers.contains(productIdentifier) } public class func canMakePayments() -> Bool { return SKPaymentQueue.canMakePayments() } public func restorePurchases() { SKPaymentQueue.default().restoreCompletedTransactions() } } // MARK: - SKProductsRequestDelegate extension IAPHelper: SKProductsRequestDelegate { public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { print("[IAPHelper] Loaded list of products...") let products = response.products products.forEach { (p) in print("[IAPHelper] Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)") } // this protocol method is not guaranteed to be dispatched on main thread DispatchQueue.main.async { self.productsRequestCompletionHandler?(true, products) self.clearRequestAndHandler() } } public func request(_ request: SKRequest, didFailWithError error: Error) { print("[IAPHelper] Failed to load list of products.") print("[IAPHelper] Error: \(error.localizedDescription)") // this protocol method is not guaranteed to be dispatched on main thread DispatchQueue.main.async { self.productsRequestCompletionHandler?(false, nil) self.clearRequestAndHandler() } } private func clearRequestAndHandler() { productsRequest = nil productsRequestCompletionHandler = nil } } // MARK: - SKPaymentTransactionObserver extension IAPHelper: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch (transaction.transactionState) { case .purchased: complete(transaction: transaction) break case .failed: fail(transaction: transaction) break case .restored: restore(transaction: transaction) break case .deferred: break case .purchasing: break @unknown default: break } } } private func complete(transaction: SKPaymentTransaction) { print("complete...") deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier) SKPaymentQueue.default().finishTransaction(transaction) } private func restore(transaction: SKPaymentTransaction) { guard let productIdentifier = transaction.original?.payment.productIdentifier else { return } print("restore... \(productIdentifier)") deliverPurchaseNotificationFor(identifier: productIdentifier) SKPaymentQueue.default().finishTransaction(transaction) } private func fail(transaction: SKPaymentTransaction) { print("fail...") if let transactionError = transaction.error as NSError?, let localizedDescription = transaction.error?.localizedDescription, transactionError.code != SKError.paymentCancelled.rawValue { print("Transaction Error: \(localizedDescription)") } NotificationCenter.default.post(name: .IAPHelperPurchaseNotificationFail, object: nil) SKPaymentQueue.default().finishTransaction(transaction) } private func deliverPurchaseNotificationFor(identifier: String?) { guard let identifier = identifier else { return } purchasedProductIdentifiers.insert(identifier) UserDefaults.standard.set(true, forKey: identifier) NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier) } }