/// 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 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("[IAPHelper] Buying product \(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] Products loaded (count: \(response.products.count))") // this protocol method is not guaranteed to be dispatched on main thread DispatchQueue.main.async { self.productsRequestCompletionHandler?(true, response.products) self.clearRequestAndHandler() } } public func request(_ request: SKRequest, didFailWithError error: Error) { print("[IAPHelper] Failed to load list of products (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 paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { print("[IAPHelper] Restore transaction completed") if queue.transactions.count == 0 { NotificationCenter.default.post(name: .EQNInAppPurchaseNoTransactions, object: nil) } } public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { print("[IAPHelper] Restore transaction failed (error : \(error.localizedDescription))") NotificationCenter.default.post(name: .EQNInAppPurchaseDidFail, object: nil) } public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { print("[IAPHelper] Updated transactions (count: \(transactions.count))") for transaction in transactions { switch (transaction.transactionState) { case .purchased: handlePurchased(transaction: transaction) case .failed: handleFailed(transaction: transaction) case .restored: handleRestored(transaction: transaction) case .deferred: break case .purchasing: break @unknown default: break } } } private func handlePurchased(transaction: SKPaymentTransaction) { print("[IAPHelper] Handle purchased transaction (product: \(transaction.payment.productIdentifier))") deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier) SKPaymentQueue.default().finishTransaction(transaction) // register subscription serverPurchaseRegistration(for: transaction.payment.productIdentifier, transactionId:transaction.transactionIdentifier) } private func handleRestored(transaction: SKPaymentTransaction) { guard let productIdentifier = transaction.original?.payment.productIdentifier else { print("[IAPHelper] Handle restored transaction falied, cannot retrieve productIdentifier") return } print("[IAPHelper] Handle restored transaction (product: \(productIdentifier))") deliverPurchaseNotificationFor(identifier: productIdentifier) SKPaymentQueue.default().finishTransaction(transaction) // register subscription serverPurchaseRegistration(for: productIdentifier, transactionId: transaction.transactionIdentifier) } private func handleFailed(transaction: SKPaymentTransaction) { print("[IAPHelper] Handle failed transaction (product: \(transaction.payment.productIdentifier))") if let transactionError = transaction.error as NSError?, let localizedDescription = transaction.error?.localizedDescription, transactionError.code != SKError.paymentCancelled.rawValue { print("[IAPHelper] Handle failed transaction error: \(localizedDescription)") } NotificationCenter.default.post(name: .EQNInAppPurchaseDidFail, 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: .EQNInAppPurchaseDidComplete, object: identifier) } private func serverPurchaseRegistration(for productId: String, transactionId: String?) { let idTransaction = transactionId ?? "NOT_AVAILABLE" let url = EQNGeneratoreURLServer.urlRegisterSubscription(forProductId: productId, transactionId: idTransaction) ServerRequest.default().inviaInformazioniAlServer(with: url, richiesta: .registerSubscription) { (response) in // nope print("[IAPHelper] Subscription registered with server") } failure: { (error) in // nope print("[IAPHelper] Error when registering subscription (error: \(String(describing: error?.localizedDescription))") } } }