I am using Storekit to implement In-App Purchase in my app. It’s live now. I have implemented a non-consumable purchase to remove ads. Now the issue is if even a new user tap on restore purchase, it’s successfully restoring the purchase which it shouldn’t. I am unable to debug it due to lack of testing device but I have asked multiple people to download the app and restore the purchase and they were successfully transitioned to Premium.
Below is the code I am using:
enum IAPHandlerAlertType {
case initialize
case setProductIds
case disabled
case restored
case purchased
case failed
case error
case restoreFailed
var message: String{
switch self {
case .error: return "An error occured"
case .initialize: return ""
case .setProductIds: return "Product ids not set, call setProductIds method!"
case .disabled: return "Purchases are disabled in your device!"
case .restored: return "You've successfully restored your purchase!"
case .purchased: return "You've successfully bought this purchase!"
case .failed: return "Failed to buy this purchase!"
case .restoreFailed: return "Failed to restore this purchase!"
}
}
}
class IAPManager: NSObject {
//MARK:- Shared Object
//MARK:-
static let shared = IAPManager()
private override init() { }
//MARK:- Properties
//MARK:- Private
fileprivate var productIds = ["com.identifier.appName.removeAds"]
fileprivate var productID = ""
fileprivate var productsRequest = SKProductsRequest()
fileprivate var fetchProductComplition: (([SKProduct])->Void)?
fileprivate var productToPurchase: SKProduct?
var purchaseProductComplition: ((IAPHandlerAlertType, Error?)->Void)?
//MARK:- Public
var isLogEnabled: Bool = true
//MARK:- Methods
//MARK:- Public
//Set Product Ids
func setProductIds(ids: [String]) {
self.productIds = ids
}
//MAKE PURCHASE OF A PRODUCT
func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
func purchase(product: SKProduct, completion: @escaping ((IAPHandlerAlertType, Error?) -> Void)) {
self.purchaseProductComplition = completion
self.productToPurchase = product
if self.canMakePurchases() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
log("PRODUCT TO PURCHASE: (product.productIdentifier)")
productID = product.productIdentifier
}
else {
completion(IAPHandlerAlertType.disabled, nil)
}
}
// RESTORE PURCHASE
func restorePurchase(){
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
// FETCH AVAILABLE IAP PRODUCTS
func fetchAvailableProducts(completion: @escaping (([SKProduct])->Void)){
self.fetchProductComplition = completion
// Put here your IAP Products ID's
if self.productIds.isEmpty {
log(IAPHandlerAlertType.setProductIds.message)
fatalError(IAPHandlerAlertType.setProductIds.message)
}
else {
productsRequest = SKProductsRequest(productIdentifiers: Set(self.productIds))
productsRequest.delegate = self
productsRequest.start()
}
}
//MARK:- Private
fileprivate func log <T> (_ object: T) {
if isLogEnabled {
NSLog("(object)")
}
}
}
//MARK:- Product Request Delegate and Payment Transaction Methods
extension IAPManager: SKProductsRequestDelegate, SKPaymentTransactionObserver {
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
if let completion = self.fetchProductComplition {
completion(response.products)
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
if let completion = self.fetchProductComplition {
completion([])
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if let completion = self.purchaseProductComplition {
completion(IAPHandlerAlertType.restored, nil)
}
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
if let completion = self.purchaseProductComplition {
completion(IAPHandlerAlertType.restoreFailed, error)
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
log("Product purchase done")
SKPaymentQueue.default().finishTransaction(trans)
if let completion = self.purchaseProductComplition {
completion(IAPHandlerAlertType.purchased, nil)
}
break
case .failed:
log("Product purchase failed")
SKPaymentQueue.default().finishTransaction(trans)
if let completion = self.purchaseProductComplition {
completion(IAPHandlerAlertType.failed, trans.error)
}
break
case .restored:
log("Product restored")
SKPaymentQueue.default().finishTransaction(trans)
if let completion = self.purchaseProductComplition {
completion(IAPHandlerAlertType.restored, nil)
}
break
default: break
}
}
}
}
}
ViewModel
func restoreAction() {
Spinner.start()
IAPManager.shared.fetchAvailableProducts { products in
if products.count > 0 {
IAPManager.shared.restorePurchase()
IAPManager.shared.purchaseProductComplition = { [self] result, error in
self.handlePurchaseRestoreResult(result: result, error: error)
Spinner.stop()
}
} else {
Spinner.stop()
}
}
}
func handlePurchaseRestoreResult(result: IAPHandlerAlertType, error: Error?) {
if error != nil {
showAlert = .init(id: .error)
return
}
switch result {
case .disabled:
showAlert = .init(id: .disabled)
case .purchased:
Defaults.isPremiumPurchased = true
Defaults.totalCoins += 1000
isPremiumPurchased = 1
break
case .restored:
Defaults.isPremiumPurchased = true
isPremiumPurchased = 1
break
case .failed:
showAlert = .init(id: .failed)
default:
break
}
}
Am I doing something wrong here?
2
Answers
You seems to be missing a validatePurchase function before giving the access to the user. Before setting the purchase status for the user, it will be better if you validate if the user has actually purchased the premium from your db. The following is the minimal implementation for a function you want to have before handing out premium to the user:-
Adding this should solve the problem you are having.
In your
paymentQueueRestoreCompletedTransactionsFinished
delegate method you are calling yourcompletionHandler
and passingIAPHandlerAlertType.restored
.In your
handlePurchaseRestoreResult
this status results in you settingisPremiumPurchased = 1
However, you have misunderstood the purpose of the
paymentQueueRestoreCompletedTransactionsFinished
method – It simply indicates that the restoration process is complete. You would typically use this to update your UI; removing an activity indicator for example.This delegate method is called regardless of whether there were any purchases to restore, so you should not set
isPremiumPurchased = 1
simply because this method was called.You should only set
isPremiumPurchased = 1
in response to a transaction being presented to your payment queue.The original StoreKit API is also being deprecated in iOS 18. You may want to consider moving to StoreKit2 API. This is much simpler to use, doesn’t require purchase restoration flows and doesn’t event need your to persist your own ‘purchased’ state – You can simply check to see if the product has been purchased.