0

I am having a problem trying to purchase an IAP as a sandbox user. It's not the sandbox account that's the problem. It's the fact that my var products = [SKProduct]() array in IAPService.swift is empty.

Inside my StoreViewController.swift (which also stores my Game Center leaderboards):

class StoreViewController: UIViewController, GKGameCenterControllerDelegate {

    @IBAction func purchase(_ sender: UIButton) {
        IAPService.shared.purchase(product: .nonConsumable)
    }

    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        IAPService.shared.getProducts()

        print(IAPService.shared.products) // This is an empty array?

        authenticateLocalPlayer() // Related to Game Center
    }
}

Inside my IAPProducts.swift

enum IAPProducts: String {
    case nonConsumable = "com.nameofgame.nameofproduct"
}

Inside my IAPService.swift

import Foundation
import StoreKit

class IAPService: NSObject {

    private override init() {}
    static let shared = IAPService()

    var products = [SKProduct]()
    let paymentQueue = SKPaymentQueue.default()

    func getProducts() {
        let products: Set = [IAPProducts.nonConsumable.rawValue,
                             ]

        let request = SKProductsRequest(productIdentifiers: products)
        request.delegate = self
        request.start()
        paymentQueue.add(self)
    }

    func purchase(product: IAPProducts) {
        guard let productToPurchase = products.filter({ $0.productIdentifier == product.rawValue }).first else { return }

        let payment = SKPayment(product: productToPurchase)
        paymentQueue.add(payment)
    }
}

extension IAPService: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        products = response.products
    }
}

extension IAPService: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {

            switch transaction.transactionState {
            case .purchased:

                if transaction.payment.productIdentifier == IAPProducts.nonConsumable.rawValue {
                    print("IAP Purchased")
                }
                break

            case .failed:
                SKPaymentQueue.default().finishTransaction(transaction)
                print(transaction.error!)
                break

            default: break
            }
        }
    }
}

Things I've done several things to try and solve the problem:

  1. Changed the Set in IAPService to an NSSet. This doesn't work because the set of Strings in let request = SKProductsRequest(productIdentifiers: products) cannot be converted to an NSSet.

  2. Checked multiple times that my BundleIDs match on Xcode and iTunes Connect. I've also done this check with the Product IDs to make sure there's a match between the IAP ID in iTunes Connect and the IAPProducts enum.

  3. I added this IAP on iTunes Connect 3 days ago, and I've checked that all my contracts for paid apps are in effect.

Please help me with this issue. Thanks.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Jack Richards
  • 133
  • 1
  • 7
  • You can't print the array straight after calling `getProducts` since the product request will complete asynchronously. Is your `didReceive` delegate method being called? Have you checked the invalid products in the response? Are you testing on a device, not a simulator? – Paulw11 Dec 07 '17 at 21:57
  • @Paulw11 How do I check if my `didReceive` method is called and how do I look at the invalid products? Yes I'm testing on a real device. – Jack Richards Dec 07 '17 at 22:02
  • Set a breakpoint in that method or event put a print statement in it. The invalid products are a property of the response object – Paulw11 Dec 07 '17 at 22:06
  • @Paulw11 I added a small print statement in the delegate method. Turns out it's not being called. How should I call it? – Jack Richards Dec 07 '17 at 22:07

1 Answers1

3

From the SKProductsRequest documentation

Note

Be sure to keep a strong reference to the request object; otherwise, the system might deallocate the request before it can complete.

Since you are creating your SKProductsRequest instance as a local variable, it is being released as soon as the getProducts function returns, before the delegate method can be called.

You need to use a property to hold a strong reference to your products request:

class IAPService: NSObject {

    private override init() {}
    static let shared = IAPService()

    var products = [SKProduct]()
    var request: SKProductsRequest?
    let paymentQueue = SKPaymentQueue.default()

    func getProducts() {
        let products: Set = [IAPProducts.nonConsumable.rawValue,
                             ]

        self.request = SKProductsRequest(productIdentifiers: products)
        request?.delegate = self
        request?.start()
        paymentQueue.add(self)
    }

    func purchase(product: IAPProducts) {
        guard let productToPurchase = products.filter({ $0.productIdentifier == product.rawValue }).first else { return }

        let payment = SKPayment(product: productToPurchase)
        paymentQueue.add(payment)
    }
}

extension IAPService: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        if !response.invalidProductIdentifiers.isEmpty {
            print("Invalid products identifiers received")
        }
        products = response.products
        self.request = nil
    }

    func request(_ request: SKRequest, didFailWithError error:Error) {
        print("Product request failed: \(error.localizedDescription)")
    }
}
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • Still doesn't work. The delegate method fails to call. Also, shouldn't `self.request = nil` be just below `products = response.products`? It won't allow you to add it where you've placed it. – Jack Richards Dec 07 '17 at 22:36
  • Sorry, that was a typo. I also suggest you add the `didFailWithError` delegate method as per my edit – Paulw11 Dec 07 '17 at 22:48
  • This was the error message I got: 'Product request failed: Cannot connect to iTunes Store'. – Jack Richards Dec 07 '17 at 22:51
  • There are some tips here: https://stackoverflow.com/questions/2359739/iphone-store-kit-cannot-connect-to-itunes-store You may want to look at `error.code` as well. – Paulw11 Dec 07 '17 at 23:09
  • Restarting Xcode and my test device solved the problem for me. Thanks for your help. – Jack Richards Dec 07 '17 at 23:24