Issue with In app purchase in iOS? - ios

I have impelmented in app purchase in my app.I have followed this tutorial
Now in my app when i try load products then it does not load.It shows error as can't connect to iTunes store.I have craeted one product on iTunes.
Below is the code
/
*
* Copyright (c) 2016 Razeware LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import StoreKit
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
open class IAPHelper : NSObject {
static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers = Set<ProductIdentifier>()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)")
} else {
print("Not purchased: \(productIdentifier)")
}
}
super.init()
SKPaymentQueue.default().add(self)
}
}
// MARK: - StoreKit API
extension IAPHelper {
public func requestProducts(completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let products = response.products
print("Loaded list of products...")
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPHelper: 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...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: 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)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
}
}
Please guide what is the issue ?
EDIT:
Did fail with error is getting called."Failed to load list of products."

Make sure you have signed out of any production iTunes accounts on the device.
I was getting this error on my test phone which was logged in with my actual iTunes account. You cannot test apps using your production iTunes account, hence the error. I just wish Apple provided a better error so as to avoid this guesswork...
The solution is to create a test user account if you don't have one. Test in-app purchase using that account.
Note: If in case, you are not getting products, please make sure that you are using product identifier while sending request exactly that matches with the product identifier on itunes.
Another silly reason, we encounter this error when we run on simulator. Check whether
Your app is running in the Simulator, which does not support In-App
Purchase

After logging out of the iTunes & iCloud account i reset my iOS device & In app purchase worked.Although i don't know why.
You don't have to be logged in to iOS device for getting products from in app purchase.For buying the products then we must be logged into the device.

Related

iOS Restoring IAP works in iOS 15, but not iOS 14.5

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.

Having trouble receiving any pop up or prompt for user sign in

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.

why non-renewing subscription, shows "This In-App purchase has already been bought. It will be restored for free.", when purchasing item again?

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.

paymentQueue - transaction fail

I am building an app with In-App Purchases. The app has 6 products for sale (all consumable).
The app is able to recognize the products (from itunes connect) available for purchase using the following code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Apple Store
appleStoreProducts = [productID, product5ID, product25ID, product100ID, product500ID, product1000ID]
SKPaymentQueue.default().add(self)
self.getProductInfo()
}
func getProductInfo() {
if SKPaymentQueue.canMakePayments() {
for product in appleStoreProducts {
let request = SKProductsRequest(productIdentifiers: NSSet(objects: product) as! Set<String>)
request.delegate = self
request.start()
}
} else {
// Tell user that In-App Purchase is disabled in settings
print("In-App Purchase is disabled in settings")
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
var products = response.products
if (products.count != 0) {
let newProduct = products[0]
switch newProduct.localizedTitle {
case "DOLLAR":
product = newProduct
case "five dollars":
product2 = newProduct
case "twenty five":
product3 = newProduct
case "one hundred dollars":
product4 = newProduct
case "thousand dollars":
product5 = newProduct
default:
print("none of the above")
}
print("titile : \(newProduct.localizedTitle) \n description: \(newProduct.localizedDescription)")
}
else {
print("Product not found")
}
let invalids = response.invalidProductIdentifiers
for product in invalids {
print("Product not found: \(product)")
}
}
Then you can click a button to make the purchase of one of the items, for example:
#IBAction func productOne(_ sender: Any) {
let payment = SKPayment(product: product!)
SKPaymentQueue.default().add(payment)
}
My PROBLEM appears next, when the paymentQueue method is called:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case SKPaymentTransactionState.purchased:
// self.pointsBought()
SKPaymentQueue.default().finishTransaction(transaction)
case SKPaymentTransactionState.failed:
SKPaymentQueue.default().finishTransaction(transaction)
print("transaction failed")
default:
print("ESTDO: \(transaction.transactionState)")
break
}
}
}
This method is called twice when I click once the button. The first time the transactionState is neither "purchased" or "failed", so it calls the default option. The second time the result is "failed". However, I don't know what am I doing wrong.
Relevant information:
I am using an iOS device (iphone 6) to run the app
I am using the developer account (of the app) in my iphone
You are failing to cover every case. You need to cover all of them, and you need to call finishTransaction for certain cases. This is the format of a correct updatedTransactions method:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for t in transactions {
switch t.transactionState {
case .purchasing, .deferred: break // do nothing
case .purchased, .restored:
let p = t.payment
if p.productIdentifier == whatever {
// ... do stuff ...
queue.finishTransaction(t)
}
case .failed:
queue.finishTransaction(t)
}
}
}

StoreKit In App Purchases seem to be crashing on specific iPhone models?

I'm trying to identify a problem that seems to be happening on iPhone 6's but not iPhone 7's. Whenever one of my iPhone 6 users taps on the button to load my in app purchase data and display the actual in app purchase, it crashes for them. This doesn't seem to be the problem for people running iPhone 7s. I can't tell if the problem is per device model or something else
I have a function called constructPurchasePanel() which brings up a panel displaying my in app purchases. The title of my in app purchases is grabbed from an array of products I have stored in my Menu Scene (which is a singleton)
constructPurchasePanel() code
nameLabel.text = SceneCoordinator.shared.menuScene?.products[2].localizedTitle
nameLabel2.text = SceneCoordinator.shared.menuScene?.products[1].localizedTitle
nameLabel3.text = SceneCoordinator.shared.menuScene?.products[0].localizedTitle
MenuScene products array initialization
var products = [SKProduct]()
MyProducts.store.requestProducts{success, products in
if success {
self.products = products!
}
}
requestProducts() function, and my other IAP related functions. 99% of this is from Ray Wenderlich's tutorial:
/*
* Copyright (c) 2016 Razeware LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import StoreKit
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
open class IAPHelper : NSObject {
static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers = Set<ProductIdentifier>()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)")
} else {
print("Not purchased: \(productIdentifier)")
}
}
super.init()
SKPaymentQueue.default().add(self)
}
}
// MARK: - StoreKit API
extension IAPHelper {
public func requestProducts(completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let products = response.products
print("Loaded list of products...")
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPHelper: 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 applyEffect(productIdentifier: String) {
switch(productIdentifier) {
case StackyChefProducts.smallCoinPackage:
SKTAudio.sharedInstance.playSoundEffect("Content/purchase.wav")
Chef.sharedInstance.money += 30000
SceneCoordinator.shared.shopScene?.updateBalance()
for item in SceneCoordinator.shared.shopScene!.endlessItems { item.updateUI() }
for item in SceneCoordinator.shared.shopScene!.upgradeItems { item.updateUI() }
case StackyChefProducts.mediumCoinPackage:
SKTAudio.sharedInstance.playSoundEffect("Content/purchase.wav")
Chef.sharedInstance.money += 90000
SceneCoordinator.shared.shopScene?.updateBalance()
for item in SceneCoordinator.shared.shopScene!.endlessItems { item.updateUI() }
for item in SceneCoordinator.shared.shopScene!.upgradeItems { item.updateUI() }
case StackyChefProducts.largeCoinPackage:
SKTAudio.sharedInstance.playSoundEffect("Content/purchase.wav")
Chef.sharedInstance.money += 150000
SceneCoordinator.shared.shopScene?.updateBalance()
for item in SceneCoordinator.shared.shopScene!.endlessItems { item.updateUI() }
for item in SceneCoordinator.shared.shopScene!.upgradeItems { item.updateUI() }
case StackyChefProducts.removeAdsProduct:
if GameData.sharedInstance.adsOn {
SKTAudio.sharedInstance.playSoundEffect("Content/purchase.wav")
GameData.sharedInstance.adsOn = false
MenuScene.removeAds.removeFromParent()
MenuScene.bannerView.removeFromSuperview()
}
case StackyChefProducts.couponProduct:
SKTAudio.sharedInstance.playSoundEffect("Content/purchase.wav")
GameData.sharedInstance.coupons += 1
SceneCoordinator.shared.grabBagScene?.updateCouponButton()
SceneCoordinator.shared.grabBagScene?.updateIAPButtons()
case StackyChefProducts.couponsProduct:
SKTAudio.sharedInstance.playSoundEffect("Content/purchase.wav")
GameData.sharedInstance.coupons += 3
SceneCoordinator.shared.grabBagScene?.updateCouponButton()
SceneCoordinator.shared.grabBagScene?.updateIAPButtons()
default:
print("applyEffect() shouldn't reach this")
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
applyEffect(productIdentifier: transaction.payment.productIdentifier)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
applyEffect(productIdentifier: transaction.payment.productIdentifier)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as NSError? {
if transactionError.code != SKErrorCode.paymentCancelled.rawValue {
print("Transaction Error: \(String(describing: transaction.error?.localizedDescription))")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
}
}
Does anyone have an idea of what could be causing this? What I think is happening is that constructPurchasePanel() gets called, and products array is empty, therefore causing the crash when I try to grab a title from one of the products. This mean something must be going wrong in requestProducts() I believe.
I know the products exist because this code works fine for me and others running iPhone 7s, and I can purchase an IAP with no trouble
EDIT:
After talking to some more users it doesn't seem like this is necessarily a device specific thing, so I'm not sure how it's crashing for some but working for others.
The app crash because products not complete load at the time you call constructPurchasePanel()
You need load products before you use it:
SceneCoordinator.shared.menuScene?.products[2].localizedTitle
Like suggest from Ray Wenderlich's tutorial, make sure you requestProducts() function before, best solution is use in viewDidAppear method.

Resources