I am making an app in SwiftUI using an In-app purchase. In this app, the user should be able to buy points as many times as he wants, so I have used consumable products. But when I’ve tried to buy them once again I got the information "This In-App purchase has already been bought. It will be restored for free". I’ve already searched for way how to do it but none of the ideas worked for me.
Here is my StoreManager class:
import Foundation
import StoreKit
import SwiftUI
class StoreManager : NSObject, ObservableObject, SKProductsRequestDelegate {
@EnvironmentObject var authViewModel: AuthViewModel
@Published var transactionState: SKPaymentTransactionState?
@Published var myProducts = [SKProduct]()
var request: SKProductsRequest!
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Did receive response")
if !response.products.isEmpty {
for fetchedProduct in response.products {
DispatchQueue.main.async {
self.myProducts.append(fetchedProduct)
}
}
for invalidIdentifier in response.invalidProductIdentifiers {
print("Invalid identifiers found: (invalidIdentifier)")
}
}else{
print("it's empty")
}
}
func getProducts(productIDs: [String]) {
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Request did fail: (error)")
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
transactionState = .purchasing
break
case .purchased:
print("purchased")
queue.finishTransaction(transaction)
transactionState = .purchased
break
case .restored:
print("restored")
transactionState = .restored
queue.finishTransaction(transaction)
break
case .failed, .deferred:
queue.finishTransaction(transaction)
transactionState = .failed
break
default:
queue.finishTransaction(transaction)
break
}
}
}
func purchaseProduct(product: SKProduct) {
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
} else {
print("User can't make payment.")
}
}
func restoreProducts() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
And I am simply using getProducts with onAppear, and purchase product on button’s action.
Please help me or if an answer to a similar question already exists send me a link to that thread.
2
Answers
This is because there are two types of in-app purchases. Consumable and
Non – Consumable. Consumable in-purchases can be made multiple times and Non -consumable in-app purchases can only be made once.
https://developer.apple.com/in-app-purchase/
Solution
Check in App Store Connect in-app purchases section to see which type of in-app purchase you created. If it was Non – Consumable, delete the existing in-app purchase and create a new one with is consumable.
Resources
Implement In App Purchase (IAP) in iOS applications
Adding In-App Purchases to a SwiftUI App
Your
StoreManager
class needs to conform toSKPaymentTransactionObserver
– You have implemented the relevant delegate method from this protocol, but you haven’t declared conformance or, most importantly, added yourStoreManager
instance as a transaction queue observer.Since you aren’t observing the transaction, you will never call
finishTransaction
– When you attempt to purchase the consumable a second time, Store Kit sees that there is an incomplete purchase of that item and so you get the message that you have already purchased it.As well as actually registering as a transaction queue observer, it is important that your app creates an instance of
StoreManager
as soon as it launches – indidFinishLaunching
or an equivalent location.This is so that any incomplete purchases from a previous run can be presented to your transaction queue observer.
You should only ever have one instance of
StoreManager
and its lifetime should be the lifetime of your app.You need something like:
Also, seeing
@EnvironmentObject var authViewModel: AuthViewModel
leads me to think that you may have a separation-of-concerns issue in your design.