Why is paymentQueue(:shouldAddStorePayment::) not being called?
I am sure I've done everything I need to do.
I declared my own class that supports the SKPaymentTransactionObserver protocol:
import UIKit
import StoreKit
import AudioToolbox.AudioServices
class UTIPaymentTransactionObserver: NSObject, SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
print("!!! shouldAddStorePayment")
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
return false
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("!!! updatedTransactions")
for transaction in transactions {
print("!!! transaction=", transaction)
switch transaction.transactionState {
// Call the appropriate custom method for the transaction state.
case SKPaymentTransactionState.purchasing:
showTransactionAsInProgress(transaction, deferred: false)
case SKPaymentTransactionState.deferred:
showTransactionAsInProgress(transaction, deferred: true)
case SKPaymentTransactionState.failed:
failedTransaction(transaction)
case SKPaymentTransactionState.purchased:
completeTransaction(transaction)
case SKPaymentTransactionState.restored:
restoreTransaction(transaction)
}
}
}
func showTransactionAsInProgress(_ transaction: SKPaymentTransaction, deferred: Bool) {
print("!!! showTransactionAsInProgress")
}
func failedTransaction(_ transaction: SKPaymentTransaction) {
print("!!! failedTransaction")
SKPaymentQueue.default().finishTransaction(transaction)
}
func completeTransaction(_ transaction: SKPaymentTransaction) {
print("!!! completeTransaction")
SKPaymentQueue.default().finishTransaction(transaction)
}
func restoreTransaction(_ transaction: SKPaymentTransaction) {
print("!!! restoreTransaction")
}
}
I added code to vibrate the device when paymentQueue(:shouldAddStorePayment::) is called to indicate that the method is actually called.
I declared an instance of the observer class globally:
internal let paymentTransactionObserver = UTIPaymentTransactionObserver()
I made sure I added the observer in AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
SKPaymentQueue.default().add(paymentTransactionObserver)
return true
}
The print statement in the paymentQueue(:shouldAddStorePayment::) method never prints and the device never vibrates. It doesn't look like that method is called.
The paymentQueue(_:updatedTransactions:) method is called. The print statement in that method executed.
In this code I returned false for the paymentQueue(:shouldAddStorePayment::) method, but it doesn't make a difference. The process goes through just as if I returned a true. The product had already been purchased before, so it goes through and lets the user/tester purchase it again.
Any help would be appreciated.
Here is code extension for the view controller that retrieves the product from App Store and presents my user interface that allows the user to purchase the product:
I call validateProductIdentifiers() to start the process of selling the product to the user.
// MARK: - SKProductsRequestDelegate
extension CloudViewController: SKProductsRequestDelegate {
func validateProductIdentifiers() {
let url = Bundle.main.url(forResource: "Purchase", withExtension: "plist")!
let nsArrayProductIdentifiers: NSArray = NSArray(contentsOf: url)!
let productIdentifiers = nsArrayProductIdentifiers as! [String]
print(productIdentifiers)
let setProductIdentifers: Set = Set(productIdentifiers)
let productsRequest = SKProductsRequest(productIdentifiers: setProductIdentifers)
self.productsRequest = productsRequest
productsRequest.delegate = self
productsRequest.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("!!! didReceive")
self.products = response.products
let alertMessage = "Would you like to purchase?"
let alert = UIAlertController(title: nil, message: alertMessage, preferredStyle: .actionSheet)
let actionYes = UIAlertAction(title: "Yes", style: .default) {
action in
// Purchase
let product: SKProduct = response.products.first!
let payment: SKMutablePayment = SKMutablePayment(product: product)
SKPaymentQueue.default().add(payment)
}
let actionNo = UIAlertAction(title: "No", style: .cancel, handler: nil)
alert.addAction(actionYes)
alert.addAction(actionNo)
alert.popoverPresentationController?.barButtonItem = barButtonItemEnableDropbox
present(alert, animated: true, completion: nil)
}
}
As per the documentation for this method
This delegate method is called when the user starts an in-app purchase in the App Store, and the transaction continues in your app. Specifically, if your app is already installed, the method is called.
This occurs when a user redeems a promo code for an in-app purchase in the App Store app or purchases a promoted in-App purchase in the App Store app.
It is not called when the purchase is initiated in your app, since you already have control over whether purchasing should be permitted when the user is in your app.
Related
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.
I'm testing a promoted In-App Purchase, as described here.
Here's how it works:
I construct the system URL.
I send the system URL link to my device.
I tap the link.
My app is automatically opened, and the In App Purchase payment sheet is presented.
I tap the "Purchase" button (or whatever it's called), and enter my password to complete the transaction.
The transaction appears to be successful and an alert appears that says, "You're all set. Your purchase was successful".
The problem is, some code that should run depending on the state of the transaction seems not to. I'm not sure how that's possible, but there you go.
So, the deferredTransactionHandler, handlePurchase, handleFailedPurchase, and handleRestoredPurchase methods appear not to be invoked.
class StoreObserver: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate {
var productRequest: SKProductsRequest?
var isAuthorizedForPayments: Bool {
return SKPaymentQueue.canMakePayments()
}
var products = [SKProduct]()
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
//The user has initiated a promoted In-App Purchase directly from the app's product page.
//Show the UI:
SceneDelegate.mainData.showAppStorePromotionUI = true
//"Return true to continue the transaction in your app. Return false to defer or cancel the transaction."
return true
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing: break
case .deferred: deferredTransactionHandler(transaction)
case .purchased: handlePurchase(transaction)
case .failed: handleFailedPurchase(transaction)
case .restored: handleRestoredPurchase(transaction)
#unknown default: fatalError("unknownPaymentTransaction")
}
}
}
}
How do I know these methods aren't being called? 1) Inside these methods, I update the UI to reflect that the transaction has completed (the UI is never updated, though), and 2) Inside these methods I try to present an alert (but the alert is never presented). Here's how I'm doing that:
func handlePurchase(_ transaction: SKPaymentTransaction) {
//Update the UI
SceneDelegate.mainData.showAppStorePromotionUI = false
//Present an alert so you know this function ran
let alert = UIAlertController(title: "Alert", message: "handlePurchase()", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
}))
let topViewController = UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.rootViewController
topViewController?.present(alert, animated: true, completion: nil)
//Complete the transaction
SKPaymentQueue.default().finishTransaction(transaction)
}
Why does it seem that these functions are not being called? What am I missing?
Thank you!
Sigh. It was a stupid mistake on my part.
I'd been testing something, and had forgotten to remove a line in my SceneDelegate's sceneWillResignActive(_:) in which I removed the observer. So, every time the payment sheet was presented, the observer was removed, which messed everything up.
func sceneWillResignActive(_ scene: UIScene) {
//Removing this line solved the problem
//SKPaymentQueue.default().remove(SceneDelegate.inAppPurchases)
}
I have the following code for in-app purchases. Since somehow there is a delay as soon as purchase() is executed, I need to indicate that the app is loading. - How can I add an activity indicator as soon as the function purchase() is called?
I've already tried to add a subview with an indicator to the current view controller inside of the given function, but I failed.
class IAPService: NSObject {
...
func purchase() {
guard let productToPurchase = products.first else { return }
print(productToPurchase)
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
func restorePurchases() {
paymentQueue.restoreCompletedTransactions()
}
}
extension IAPService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.products = response.products
for product in response.products {
print(product.localizedTitle)
}
}
}
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(), transaction.payment.productIdentifier)
if transaction.payment.productIdentifier == "com.timfuhrmann.savum.premium" && ( transaction.transactionState == .purchased || transaction.transactionState == .restored ) {
premiumPurchased = true
defaults.set(premiumPurchased, forKey: "premiumPurchased")
print(premiumPurchased)
}
switch transaction.transactionState {
case .purchasing: break
case .purchased: goToAddCar(); queue.finishTransaction(transaction)
default: queue.finishTransaction(transaction)
}
}
}
}
As a class, IAPService should not care, or even know, about the user interface. The activity indicator should be initiated and controlled entirely by the view controller.
I would use NotificationCenter observers to let the view controller know when the transaction is complete (or errors out) so the VC knows when to disable the activity indicator and update the rest of the UI.
You can show an activity indicator in the ViewController before that function is called and include a completion handler in the function where you dismiss the activity indicator in the ViewController when the purchase is completed.
You should add the activity indicator inside the ViewController, not from a function that is in you payments module, since the payment module is responsible only for making and processing payments.
I have followed a walkthrough to include a non-consumable in-app purchase. Unfortunately, nothing is happening at all after the user clicks the button for the purchase. I am not getting any prompt to log in to iTunes or to accept the payment. Is there something I am missing here?
I have tried multiple walkthroughs and they all seem to have the similar code, I have followed the steps through the apple website, however I am unsure as to whether I have to do a full app submission before I can test the in-app purchases through a sandbox.
IAPService.swift
import Foundation
import StoreKit
import UIKit
class IAPService: NSObject {
private override init() {}
static let shared = IAPService()
var products = [SKProduct]()
let paymentQueue = SKPaymentQueue.default()
func getProducts() {
let products: Set = [IAPProduct.nonConsumable.rawValue]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
paymentQueue.add(self)
}
func purchase(product: IAPProduct) {
guard let productToPurchase = products.filter({
$0.productIdentifier == product.rawValue }).first else { return }
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
func restorePurchases() {
print("restore purchases")
paymentQueue.restoreCompletedTransactions()
}
}
extension IAPService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive
response: SKProductsResponse) {
products = response.products
for product in response.products {
print(product.localizedTitle)
}
}
}
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions
transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(),
transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing: break
default: queue.finishTransaction(transaction)
}
}
}
}
extension SKPaymentTransactionState {
func status() -> String {
switch self {
case .deferred: return "deferred"
case .failed: return "failed"
case .purchased: return "purchased"
case .purchasing: return "purchasing"
case .restored: return "restored"
}
}
}
In my products file
IAP.Products.swift
import Foundation
enum IAPProduct: String {
case nonConsumable = "Quizly"
}
In my mainVC
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
IAPService.shared.getProducts()
print("IAP == \(IAPService.shared.products)") // Why is this an empty array?
}
#objc func pressToGetPremium(_ sender : UIButton) {
IAPService.shared.purchase(product: .nonConsumable)
print("IAP ===== \(IAPService.shared.products)")
}
When the user clicks the button I was hoping that a pop up would come up first making the user have to sign in to their apple account and then another pop up would ask if they wanted to accept the non-consumable product ($1.99) etc. But I am not getting a pop up at all.
I am receiving this back from didReceive products......
response SKProductsResponse 0x00000002811cba10
baseNSObject#0 NSObject
isa Class 0x2811cba10 0x00000002811cba10
_internal SKProductsResponseInternal * 0x28139c0e0 0x000000028139c0e0
NSObject NSObject
_invalidIdentifiers __NSSingleObjectArrayI * 1 element 0x00000002811cb930
[0] __NSCFString * "Quizly" 0x000000028139c620
NSMutableString NSMutableString
_products __NSArray0 * 0 elements 0x00000002811c0050
NSArray NSArray
NSObject NSObject
isa Class __NSArray0 0x000001a25a605811
As per our comment discussion - The error produced from the didReceive products method indicates your bundleIdentifiers are not matching the ones on the appStore.
I am implementing the non renewable purchase in my app. I am still using in sandbox mode. After I subscribe for the product, when I again try to subscribe the same product, it gives me an alert saying ‘This In-App purchase has already been bought. It will be restored for free.’. I don’t know how I should enable user to subscribe again.
How can I handle multiple user on same device? If one user has paid for the subscription and another user log in into same device to my application he/she should not get the alert as above.
Code :
import StoreKit
class className: SKProductsRequestDelegate
{
var productIDs: Array<String?> = []
var productsArray: Array<SKProduct?> = []
override func viewDidLoad(){
// product IDs for in-app purchase products
productIDs.append(“monthly_subscription_id”) // Monthly
productIDs.append(“yearly_subscription_id”) // Year
requestProductInfo()
SKPaymentQueue.default().add(self)
}
func requestProductInfo() {
if SKPaymentQueue.canMakePayments() {
let productIdentifiers = NSSet(array: productIDs)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers as Set<NSObject> as! Set<String>)
productRequest.delegate = self
productRequest.start()
}
else {
print("Cannot perform In App Purchases.")
}
}
// MARK: SKProductsRequestDelegate method implementation
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if response.products.count != 0 {
for product in response.products {
productsArray.append(product )
}
}
else {
print("There are no products.")
}
if response.invalidProductIdentifiers.count != 0 {
print(response.invalidProductIdentifiers.description)
}
}
// MARK: Buy Subscription button action
#IBAction func btn_purchase_Action(_ sender: Any){
let payment = SKPayment(product: self.productsArray[productIndex]!)
SKPaymentQueue.default().add(payment)
self.transactionInProgress = true
}
}
// MARK: - SKPaymentTransactionObserver
extension className: 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...")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction){
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(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)
}
}
I could see popup saying in-app purchase is successful, but "updatedTransaction" function is not called when i successfully finish in-app purchase process.
First time in-app purchase is completed but when i try to purchase the same product again it shows the alert that product is already purchased and could restore for free.
From your code it looks like your transaction observer is a view controller.
If the view controller is dismissed before the payment transaction has been processed then you won't get a chance to complete the transaction.
Your payment queue observer should be an object that is instantiated as soon as your app launches and remains in memory for the lifetime of your app.
Creating the payment queue observer in didFinishLaunching and holding a reference to it in your app delegate is one approach that you can use.