I'm trying to setup IAP for the first time and I am having problems with the Restore functionality. It works fine in iOS 15 using...
let refresh = SKReceiptRefreshRequest()
refresh.delegate = self
refresh.start()
...
func requestDidFinish(_ request: SKRequest) {
if request is SKReceiptRefreshRequest {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
request.cancel()
}
...but when I test on an iPhone 11 simulator running iOS 14.5 the restoreCompletedTransactions
method is reached but no updates are triggered in paymentQueue
's updatedTransactions
delegate method.
I've also noticed that if the iCloud account is not logged in, it doesn't trigger an authentication (which the documentation says should happen).
Why does the restore code work for iOS 15, but not iOS 14.5?
and
[Optional, but possibly related:] How do I trigger the authentication check for iCloud while restoring?
It's not shown below, but the view has a spinner which starts at the beginning of the restore and is ended by the completionBlock
passed along when the process starts in the restore:purchase:completion
method. There's also a modal alert that reports the results when completed. Neither of these are triggering in iOS 14.5.
This is the full class I'm doing the restore in...
import StoreKit
final class PurchaseManager: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate, SKRequestDelegate, CanCreatePopUpMessage {
// MARK: - Properties
var products = [SKProduct]()
var isTesting = false
var completion: OptionalBlock = nil
var productToRestore: Product?
var productsRestored = [Product]()
var failedRestores = [Product]()
// MARK: - Properties: Static
static var shared = PurchaseManager()
// MARK: - Functions
func restore(purchase: Product, complete: OptionalBlock = nil) { // <-- Starts here.
self.completion = complete
self.productToRestore = purchase
let refresh = SKReceiptRefreshRequest()
refresh.delegate = self
refresh.start() // <-- This concludes in requestDidFinish below...
// SKPaymentQueue.default().add(self)
// SKPaymentQueue.default().restoreCompletedTransactions()
// if #available(iOS 15.0, *) {
// let _ = Task {
// await refreshPurchasedProducts()
// }
// }
}
...
private func restoreFollowUp() {
for product in productsRestored {
handleRestore(product)
}
completion?()
guard let p = productToRestore else { return }
restoreUpdateAlert(for: p, didFail: !productsRestored.contains(p))
}
private func handleRestore(_ product: Product) {
switch product {
case .unlock(let gameMode):
switch gameMode {
case .defense:
TrenchesScene.current.infiniteBullets = true
TrenchesScene.current.pushAmmo()
case .offense:
TrenchesScene.current.unlimitedInfantry = true
TrenchesScene.current.pushUnitCounts()
}
default: break
}
}
private func getProduct(from transaction: SKPaymentTransaction) -> Product? {
getProduct(from: transaction.payment.productIdentifier)
}
private func getProduct(from transactionId: String) -> Product? {
switch transactionId {
case PurchaseId.coin4000 : return .coins(4000)
case PurchaseId.infiniteAmmo : return .unlock(.defense)
case PurchaseId.unlimitedInfantry: return .unlock(.offense)
default : return nil
}
}
...
@available(iOS 15.0, *)
func refreshPurchasedProducts() async {
self.productsRestored = []
self.failedRestores = []
for await verificationResult in Transaction.currentEntitlements {
switch verificationResult {
case .verified(let transaction):
NSLog(" #$ refreshPurchasedProducts verified: \(transaction.productID)")
if let p = getProduct(from: transaction.productID) {
productsRestored.append(p)
}
case .unverified(let unverifiedTransaction, let verificationError):
NSLog(" #$ refreshPurchasedProducts unverified: \(unverifiedTransaction.productID),\n #$ error: \(verificationError)")
if let p = getProduct(from: unverifiedTransaction.productID) {
failedRestores.append(p)
}
}
}
restoreFollowUp()
}
// MARK: - Functions: SKRequestDelegate
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print(" #$ Restore completed transaction count:\(queue.transactions.count)")
for transaction in queue.transactions {
print(" #$ completed transaction: \(transaction.payment.productIdentifier)")
}
}
// MARK: - Functions: SKPaymentTransactionObserver
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { transaction in
switch transaction.transactionState {
case .purchased: ...
case .purchasing: ...
case .restored:
print(" #$ update restoring: \(transaction.payment.productIdentifier)")
if let p = getProduct(from: transaction) {
productsRestored.append(p)
}
if transaction.transactionIdentifier == transactions.last?.transactionIdentifier {
restoreFollowUp()
}
queue.finishTransaction(transaction)
case .failed: ...
case .deferred: ...
@unknown default: ...
}
}
}
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
NSLog(" #$ Product requests removed: \(transactions.map({ $0.payment.productIdentifier }))")
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
for transaction in queue.transactions {
print(" #$ failed transaction: \(transaction.original?.transactionIdentifier ?? "nil")")
}
}
func requestDidFinish(_ request: SKRequest) {
if request is SKReceiptRefreshRequest {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
request.cancel()
}
// MARK: - Functions: SKProductsRequestDelegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
}
func request(_ request: SKRequest, didFailWithError error: Error) {
guard request is SKProductsRequest else { return }
// TODO: Handle errors
print(" #$ Product request failed? \(error.localizedDescription)")
}
}
The issue wasn't OS version at all, but it had to do with testing on simulators (as @paulw11 mentioned in comments).
Under normal conditions, a user with the same apple id on multiple devices should be able to do a purchase on one and then restore on the other. THIS IS NOT HOW IT WORKS ON SIMULATORS.
I assumed that I could just hit restore on a new simulator and that it should retrieve transactions, but for testing purposes simulators need to have the purchase happen on each device, regardless of account.