Reputation: 2343
I'm doing my first In-App purchase and a weird behaviour is that an consumable product is being restored instead of creating new transaction.
I've followed the tuto https://www.raywenderlich.com/122144/in-app-purchase-tutorial
Which I found an elegant approach.
here is my StoreKit helper:
import StoreKit
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
open class IAPHelper : NSObject {
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers: Set<ProductIdentifier> = Set()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
super.init()
SKPaymentQueue.default().add(self)
}
}
// 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("Loaded list of products...")
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
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
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
validateReceipt()
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 {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
}
}
I'm able to buy once, second it is restored automatically:
This In-App purchase has already been bought. It will be restored for free
When I get this message, none of IAPHelper methods is called.
My iTunes showing it is a consumable:
Even uninstalling the app the purchased still being restored.
It really looks like an Apple bug as my firsts tests I could buy 2..3 times without this message.
If it is not a bug, how can I prevent this situation ?
Upvotes: 7
Views: 1078
Reputation: 2343
For those stuck in this silly situation, here are some WA and solution:
queue.finishTransaction(transaction)
IMPORTANT: Finish the transaction in your paymentQueue
method:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
queue.finishTransaction(transaction)
break
case .failed:
fail(transaction: transaction)
queue.finishTransaction(transaction)
break
case .restored:
restore(transaction: transaction)
queue.finishTransaction(transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
This will avoid the situation.
If you are already stuck on it, here is the work around, just for tests purposes !
for transaction in SKPaymentQueue.default().transactions {
print(transaction)
SKPaymentQueue.default().finishTransaction(transaction)
}
You can place it on a temporary button and clear all transactions while testing in one tap.
At least it saved my life today.
Upvotes: 10