I am presenting IAP options in an alert view, and here is the code that I use to do that:
paidAlert.addAction(UIAlertAction(title: "Purchase", style: UIAlertAction.Style.default, handler: { (action) in
IAPHandler.shared.purchaseMyProduct(index: 0)
}))
paidAlert.addAction(UIAlertAction(title: "Restore", style: UIAlertAction.Style.default, handler: { (action) in
IAPHandler.shared.restorePurchase()
}))
paidAlert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { (action) in
}))
I handle the actual IAP in another Swift file, which is posted here:
import UIKit
import StoreKit
enum IAPHandlerAlertType{
case disabled
case restored
case purchased
func message() -> String{
switch self {
case .disabled:
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
notificationFeedbackGenerator.prepare()
notificationFeedbackGenerator.notificationOccurred(.error)
return "Purchases are disabled on your device."
case .restored:
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
notificationFeedbackGenerator.prepare()
notificationFeedbackGenerator.notificationOccurred(.success)
return "Unlimited strategies have been successfully unlocked. You can restore this purchase using this Apple ID on other devices."
case .purchased:
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
notificationFeedbackGenerator.prepare()
notificationFeedbackGenerator.notificationOccurred(.success)
return "Unlimited strategies have been successfully unlocked. You can restore this purchase using this Apple ID on other devices."
}
}
}
class IAPHandler: NSObject {
static let shared = IAPHandler()
let NON_CONSUMABLE_PURCHASE_PRODUCT_ID = "com.isaranjha.opc.fullversion"
fileprivate var productID = ""
fileprivate var productsRequest = SKProductsRequest()
fileprivate var iapProducts = [SKProduct]()
var purchaseStatusBlock: ((IAPHandlerAlertType) -> Void)?
// MARK: - MAKE PURCHASE OF A PRODUCT
func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
func purchaseMyProduct(index: Int){
if iapProducts.count == 0 { return }
if self.canMakePurchases() {
let product = iapProducts[index]
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
print("PRODUCT TO PURCHASE: \(product.productIdentifier)")
productID = product.productIdentifier
} else {
purchaseStatusBlock?(.disabled)
}
}
// MARK: - RESTORE PURCHASE
func restorePurchase(){
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
// MARK: - FETCH AVAILABLE IAP PRODUCTS
func fetchAvailableProducts(){
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects: NON_CONSUMABLE_PURCHASE_PRODUCT_ID
)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
}
extension IAPHandler: SKProductsRequestDelegate, SKPaymentTransactionObserver{
// MARK: - REQUEST IAP PRODUCTS
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
if (response.products.count > 0) {
iapProducts = response.products
for product in iapProducts{
let numberFormatter = NumberFormatter()
numberFormatter.formatterBehavior = .behavior10_4
numberFormatter.numberStyle = .currency
numberFormatter.locale = product.priceLocale
let price1Str = numberFormatter.string(from: product.price)
print(product.localizedDescription + "\nfor just \(price1Str!)")
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
purchaseStatusBlock?(.restored)
}
// MARK:- IAP PAYMENT QUEUE
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
print("purchased")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
purchaseStatusBlock?(.purchased)
break
case .failed:
print("failed")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
case .restored:
print("restored")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
default: break
}}}
}
}
However, multiple users (but not all users) have reported that they are able to purchase the IAP fine (it charges them and everything), but they do not get any confirmation that I have coded in (alert saying that the purchase was successful etc.) and therefore none of the purchased features. The only workaround we've been able to find is that the user needs to kill the app from multitasking, get to the initial IAP purchase alert again, but select Restore, and then it prompts accordingly and the features are unlocked.
I can't figure out where I am going wrong with this, but seeing as how Restore functionality seems to work, I'm assuming I'm missing something for actual purchasing that will call the case .purchased properly and display the alert.
Any help is appreciated.
Related
Consumable In app purchase products are returned as zero products by 'SKProductsResponse' and added into 'invalidProductIdentifiers' property.
I'm not adding products at first time, three weeks before In-app purchase was worked as expected. Suddenly it's not working for me.
Consumable In-app purchase product configuration in app store connect as below,
I have created a product with product ID as 'com.companyname.appname.productID' (eg: com.XXXXX.YYYYY.p1)
Agreement, Tax, Banking informations are filled correctly and documents are uploaded on July-2022. Also my bank account status is 'Clear' and tick mark added 'In use' column.
Since I'm testing in app purchase using 'Ad Hoc' profile screen shots are not added in product information section.
Xcode setup
Xcode version 14.1.
'Automatically manage signing' selected under Signing & Capabilities.
'Automatic' selected for both 'debug' and 'release' in provisioning profile specifier under 'Build Setting'.
import Foundation
import StoreKit
enum IAPHandlerAlertType {
case setProductIds
case disabled
case restored
case purchased
var message: String{
switch self {
case .setProductIds: return "Product ids not set, call setProductIds method!"
case .disabled: return "Purchases are disabled in your device!"
case .restored: return "You've successfully restored your purchase!"
case .purchased: return "You've successfully bought this purchase!"
}
}
}
class InAppPurchaseHandler: NSObject {
//MARK:- Shared Object
//MARK:-
static let shared = InAppPurchaseHandler()
private override init() { }
//MARK:- Properties
//MARK:- Private
fileprivate var productIds = [String]()
fileprivate var productID = ""
fileprivate var productsRequest = SKProductsRequest()
fileprivate var fetchProductComplition: (([SKProduct])->Void)?
fileprivate var productToPurchase: SKProduct?
fileprivate var purchaseProductComplition: ((IAPHandlerAlertType, SKProduct?, SKPaymentTransaction?)->Void)?
//MARK:- Public
var isLogEnabled: Bool = true
//MARK:- Methods
//MARK:- Public
//Set Product Ids
func setProductIds(ids: [String]) {
self.productIds = ids
}
//MAKE PURCHASE OF A PRODUCT
func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
func purchase(product: SKProduct, complition: #escaping ((IAPHandlerAlertType, SKProduct?, SKPaymentTransaction?)->Void)) {
self.purchaseProductComplition = complition
self.productToPurchase = product
if self.canMakePurchases() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
log("PRODUCT TO PURCHASE: \(product.productIdentifier)")
productID = product.productIdentifier
}
else {
complition(IAPHandlerAlertType.disabled, nil, nil)
}
}
// RESTORE PURCHASE
func restorePurchase(){
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
// FETCH AVAILABLE IAP PRODUCTS
func fetchAvailableProducts(complition: #escaping (([SKProduct])->Void)){
self.fetchProductComplition = complition
// Put here your IAP Products ID's
if self.productIds.isEmpty {
log(IAPHandlerAlertType.setProductIds.message)
fatalError(IAPHandlerAlertType.setProductIds.message)
}
else {
productsRequest = SKProductsRequest(productIdentifiers: Set(self.productIds))
productsRequest.delegate = self
productsRequest.start()
}
}
//MARK:- Private
fileprivate func log <T> (_ object: T) {
if isLogEnabled {
NSLog("\(object)")
}
}
}
//MARK:- Product Request Delegate and Payment Transaction Methods
//MARK:-
extension InAppPurchaseHandler: SKProductsRequestDelegate, SKPaymentTransactionObserver{
// REQUEST IAP PRODUCTS
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
if (response.products.count > 0) {
if let complition = self.fetchProductComplition {
complition(response.products)
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if let complition = self.purchaseProductComplition {
complition(IAPHandlerAlertType.restored, nil, nil)
}
}
// IAP PAYMENT QUEUE
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
log("Product purchase done")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
if let complition = self.purchaseProductComplition {
complition(IAPHandlerAlertType.purchased, self.productToPurchase, trans)
}
break
case .failed:
log("Product purchase failed")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
case .restored:
log("Product restored")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
default: break
}
}
}
}
}
Called the following two methods in viewDidLoad() of my respective viewController class
func setIAPProductID(){
InAppPurchaseHandler.shared.setProductIds(ids: self.productIDs)
}
where, 'self.productIDs' is array of string product ids.
func fetchAvailableIAPProducts(){
InAppPurchaseHandler.shared.fetchAvailableProducts { [weak self](products) in
guard let sSelf = self else {return}
sSelf.arrayProductIDs = products
}
}
where 'sSelf.arrayProductIDs' is array of SKProducts.
On my live app users keep getting this error for consumable products. This is very random error and happens rarely.
This In-App Purchase has already been bought. It will be restored for free.
In my app I've prevented users tapping on Buy Now button unless app purchase process is completed.
I've already read solution provided on following questions
Sandbox trying to restore consumable IAP
My IAP isn't working. Bugs at func Paymentqueue
I've SKPaymentQueue.default().add() at two places in my code as shown below. I'm also calling SKPaymentQueue.default().finishTransaction(transaction) for each transactionState.
Can anyone let me know what else I need to check to fix this issue?
open class IAPHelper: NSObject {
// Callback
var purchaseStatusBlock: ((IAPHandlerAlertType, String, NSData) -> Void)?
var purchaseFailed: ((SKPaymentTransaction) -> Void)?
private let productIdentifiers: Set<ProductIdentifier>
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
super.init()
SKPaymentQueue.default().add(self) // #1
}
}
And second one is
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, vc: UIViewController) {
let viewController = vc as! PurchaseViewController
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment) // #2
}
}
Transaction
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) {
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = NSData(contentsOf: receiptURL!)
if (receipt == nil) {
// No local receipt -- handle the error
let alert = UIAlertController(title: "Purchase Error", message: "No local receipt", preferredStyle: UIAlertController.Style.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertAction.Style.default) { (action) in
}
alert.addAction(okAction)
return
}
// Callback
purchaseStatusBlock?(.purchased, transaction.payment.productIdentifier, receipt!)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
}
// Callback
purchaseFailed?(transaction)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
// purchasedProductIdentifiers.insert(identifier)
// UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}
We had a similar issue bugging us for a long time...
When users initiated a purchase and then lost Internet connection or killed the app before the transaction was fully processed, they would be charged but never receive the IAP content even upon restoring
Solution
Follow Apple's best practices, and add the transaction observer at app launch đź‘Ť
In your case
You should remove:
SKPaymentQueue.default().add(self) // #1
from your IAPHelper.init method.
And instead add the observer in the AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
let iapHelper = IAPHelper()
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
SKPaymentQueue.default().add(iapHelper)
}
Then in the ViewController where you need it, you can access the iapHelper using:
let iapHelper = (UIApplication.shared.delegate as! AppDelegate).iapHelper
Please advise! I am attempting to test the in-app purchase for a sandbox user.
I am following the tutorials at the bottom of the post.
The current problem is receiving invalid product identifier via response.invalidProductIdentifiers.count != 0 in func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) .
Here is the purchase info
Here is the app info
Here is how the in-app purchase in Ready to Submit , and added to the app:
Here is the setup in Xcode:
I have checked in Settings -> Capabilities -> In-app purchase On.
The productId I am using in code is:
let PREMIUM_PRODUCT_ID = "com.iaptutorial.premium".
and then calling it like this:
// MARK: - FETCH AVAILABLE IAP PRODUCTS
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects: PREMIUM_PRODUCT_ID )
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
I also cannot sign-in before the app with the sandbox user as per here but it should not be a problem: iTunes account creation not allowed when trying to test In-App Purchases
Here are the tutorials:
https://code.tutsplus.com/tutorials/in-app-purchase-tutorial-with-swift-3-ios-sdk--cms-27595
http://www.appcoda.com/in-app-purchase-tutorial/
I am testing on a real iPad 2 iOS 9.0
I have read many SO posts about in-app purchase problems and nothing works.
I can upload the project on github if needed.
What is the issue? Thank you
Here is my code:
import UIKit
import StoreKit
class ViewController: UIViewController, SKProductsRequestDelegate,SKPaymentTransactionObserver {
#IBAction func actionPurchase(_ sender: Any) {
purchaseMyProduct(product: iapProducts[0])
}
#IBAction func actionRestore(_ sender: Any) {
}
#IBOutlet weak var premiumLabel: UILabel!
#IBOutlet weak var nonconsumableLabel: UILabel!
/* Variables */
let PREMIUM_PRODUCT_ID = "com.iaptutorial.premium"
var productID = ""
var productsRequest = SKProductsRequest()
var iapProducts = [SKProduct]()
var nonConsumablePurchaseMade = UserDefaults.standard.bool(forKey: "nonConsumablePurchaseMade")
override func viewDidLoad() {
// Check your In-App Purchases
print("NON CONSUMABLE PURCHASE MADE: \(nonConsumablePurchaseMade)")
if nonConsumablePurchaseMade {
premiumLabel.text = "Premium version PURCHASED!"
}
else {
premiumLabel.text = "Premium version LOCKED!"
}
// Fetch IAP Products available
fetchAvailableProducts()
}
// MARK: - FETCH AVAILABLE IAP PRODUCTS
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects: PREMIUM_PRODUCT_ID )
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
// MARK: - RESTORE NON-CONSUMABLE PURCHASE BUTTON
#IBAction func restorePurchaseButt(_ sender: Any) {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
nonConsumablePurchaseMade = true
UserDefaults.standard.set(nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade")
UIAlertView(title: "IAP Tutorial",
message: "You've successfully restored your purchase!",
delegate: nil, cancelButtonTitle: "OK").show()
}
// MARK: - REQUEST IAP PRODUCTS
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
print("inside productsRequest")
if (response.products.count > 0) {
iapProducts = response.products
/*
// 1st IAP Product (Consumable) ------------------------------------
let firstProduct = response.products[0] as SKProduct
// Get its price from iTunes Connect
let numberFormatter = NumberFormatter()
numberFormatter.formatterBehavior = .behavior10_4
numberFormatter.numberStyle = .currency
numberFormatter.locale = firstProduct.priceLocale
let price1Str = numberFormatter.string(from: firstProduct.price)
// Show its description
consumableLabel.text = firstProduct.localizedDescription + "\nfor just \(price1Str!)"
// ------------------------------------------------
*/
/*
// 2nd IAP Product (Non-Consumable) ------------------------------
if response.products.count > 0{
print("inside")
let secondProd = response.products[0] as SKProduct
// Get its price from iTunes Connect
let numberFormatter = NumberFormatter()
numberFormatter.locale = secondProd.priceLocale
let price2Str = numberFormatter.string(from: secondProd.price)
nonconsumableLabel.text = secondProd.localizedDescription + " for just \(price2Str!)"
}
*/
}
if response.invalidProductIdentifiers.count != 0 {
print("invalid")
print(response.invalidProductIdentifiers.description)
}
}
// MARK: - MAKE PURCHASE OF A PRODUCT
func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
func purchaseMyProduct(product: SKProduct) {
print("inside purchasemyproduct")
if self.canMakePurchases() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
print("PRODUCT TO PURCHASE: \(product.productIdentifier)")
productID = product.productIdentifier
// IAP Purchases dsabled on the Device
} else {
UIAlertView(title: "IAP Tutorial",
message: "Purchases are disabled in your device!",
delegate: nil, cancelButtonTitle: "OK").show()
}
}
// MARK:- IAP PAYMENT QUEUE
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print ("inside paymentqueue")
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
// The Consumable product (10 coins) has been purchased -> gain 10 extra coins!
if productID == PREMIUM_PRODUCT_ID {
// Save your purchase locally (needed only for Non-Consumable IAP)
nonConsumablePurchaseMade = true
UserDefaults.standard.set(nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade")
premiumLabel.text = "Premium version PURCHASED!"
UIAlertView(title: "IAP Tutorial",
message: "You've successfully unlocked the Premium version!",
delegate: nil,
cancelButtonTitle: "OK").show()
}
break
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
default: break
}}}
}
}
I am trying to incorporate an in-app purchase. For some reason I am getting a fatal error that states 'fatal error: Index out of range' with an array that holds the SKProduct products. I have one nonconsumable in app purchase, but may decide to add more later. I am trying to access the PREMIUM_PRODUCT_ID product so I can make a purchase. The error occurs when the purchaseMyProduct function is called. Any ideas in why the iapProducts array is out of bounds? Thanks for your help!
The fatal error occurs on this line purchaseMyProduct(product: iapProducts[0])
import UIKit
import StoreKit
class Settings: UIViewController, SKProductsRequestDelegate,
SKPaymentTransactionObserver {
let PREMIUM_PRODUCT_ID = "---------------"
var productID = ""
var productsRequest = SKProductsRequest()
var iapProducts = [SKProduct]()
var nonConsumablePurchaseMade = UserDefaults.standard.bool(forKey: "nonConsumablePurchaseMade")
#IBOutlet weak var adsBtn: UIButton!
#IBAction func restorePurchase(_ sender: Any) {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
// Check your In-App Purchases
print("NON CONSUMABLE PURCHASE MADE: \(nonConsumablePurchaseMade)")
// Fetch IAP Products available
fetchAvailableProducts()
UIAlertView(title: "IAP Tutorial",
message: "You've successfully restored your purchase!",
delegate: nil, cancelButtonTitle: "OK").show()
}
#IBAction func review(_ sender: Any) {
UIApplication.shared.openURL(NSURL(string: "-----------------")! as URL)
}
#IBAction func removeAds(_ sender: Any) {
//UIApplication.shared.openURL(NSURL(string: "----------------")! as URL)
purchaseMyProduct(product: iapProducts[0])
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
...
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
nonConsumablePurchaseMade = true
UserDefaults.standard.set(nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade")
/*UIAlertView(title: "IAP Tutorial",
message: "You've successfully restored your purchase!",
delegate: nil, cancelButtonTitle: "OK").show()*/
}
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects:
PREMIUM_PRODUCT_ID
)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
if (response.products.count > 0) {
iapProducts = response.products
// 1st IAP Product (Consumable) ------------------------------------
let firstProduct = response.products[0] as SKProduct
// Get its price from iTunes Connect
let numberFormatter = NumberFormatter()
numberFormatter.formatterBehavior = .behavior10_4
numberFormatter.numberStyle = .currency
numberFormatter.locale = firstProduct.priceLocale
let price1Str = numberFormatter.string(from: firstProduct.price)
// Show its description
//consumableLabel.text = firstProduct.localizedDescription + "\nfor just \(price1Str!)"
// ------------------------------------------------
// 2nd IAP Product (Non-Consumable) ------------------------------
let secondProd = response.products[0] as SKProduct
// Get its price from iTunes Connect
numberFormatter.locale = secondProd.priceLocale
let price2Str = numberFormatter.string(from: secondProd.price)
// Show its description
//nonConsumableLabel.text = secondProd.localizedDescription + "\nfor just \(price2Str!)"
// ------------------------------------
}
}
func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
func purchaseMyProduct(product: SKProduct) {
if self.canMakePurchases() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
print("PRODUCT TO PURCHASE: \(product.productIdentifier)")
productID = product.productIdentifier
// IAP Purchases dsabled on the Device
} else {
UIAlertView(title: "IAP Tutorial",
message: "Purchases are disabled in your device!",
delegate: nil, cancelButtonTitle: "OK").show()
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
// The Consumable product (10 coins) has been purchased -> gain 10 extra coins!
if productID == PREMIUM_PRODUCT_ID {
// Save your purchase locally (needed only for Non-Consumable IAP)
nonConsumablePurchaseMade = true
UserDefaults.standard.set(nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade")
//premiumLabel.text = "Premium version PURCHASED!"
UIAlertView(title: "IAP Tutorial",
message: "You've successfully unlocked the Premium version!",
delegate: nil,
cancelButtonTitle: "OK").show()
}
break
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
default: break
}}}
}
}
Your code is way too long, so I took the liberty of making an educated guess what is relevant:
import UIKit
import StoreKit
class Settings: UIViewController, SKProductsRequestDelegate,
SKPaymentTransactionObserver {
var iapProducts = [SKProduct]()
#IBAction func removeAds(_ sender: Any) {
purchaseMyProduct(product: iapProducts[0])
}
}
When you declare iapProducts, you are creating an empty array that contains no values.
Then, in your removeAds function, you are telling it to do something with the first element.
I am going to take a wild guess that nothing exists, so you are forcing it to access an object that doesn't exist.
Try the following and see if it fixes your issue.
var iapProducts: [SKProduct]?
#IBAction func removeAds(_ sender: Any) {
guard let product = iapProducts?.first else { return }
purchaseMyProduct(product: product)
}
This accomplishes two things:
It makes your [SKProduct] array an optional
The guard statement safely unwraps the first product, so only if one exists, will it try to call your purchaseMyProduct function. Otherwise, it just exits
So I know the problem is the ViewController with the IAP, but unsure how to fix it.
I add SKPaymentQueue.defaultQueue().addTransactionObserver(self) in viewDidLoad and have the delegates for the class; SKProductsRequestDelegate, SKPaymentTransactionObserver.
I use the methods to carry out transactions for purchases, restore any purchases and make any changes needed for non consumable purchases.
But regardless what the user does, after loading the IAPViewController the app keeps prompting the user to login. Its mostly when the user exits the app then opens it back up.
I tried adding SKPaymentQueue.defaultQueue().removeTransactionObserver(self) when dismissing the view however that didn't seem to work.
Does anyone have any suggestions ?? I had to pull my app from review because of this bug :(
Here's the full code;
class IAPViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
var productIDs: Array<String!> = []
var productsArray: Array<SKProduct!> = []
var selectedProductIndex: Int!
var transactionInProgress = false
override func viewDidLoad() {
super.viewDidLoad()
productIDs.append("com.COMPANY.NAME.BUY01")
productIDs.append("com.COMPANY.NAME.BUY02")
productIDs.append("com.COMPANY.NAME.BUY03")
productIDs.append("com.COMPANY.NAME.BUY04")
requestProductInfo()
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
}
#IBAction func purchase04(sender: AnyObject) {
selectedProductIndex = 0
transactionAction()
}
#IBAction func restoreAction(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
//StoreKit
func transactionAction() {
let payment = SKPayment(product: self.productsArray[self.selectedProductIndex] as SKProduct)
SKPaymentQueue.defaultQueue().addPayment(payment)
self.transactionInProgress = true
}
//Request Products
func requestProductInfo() {
if SKPaymentQueue.canMakePayments() {
let productIdentifiers = NSSet(array: productIDs)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productRequest.delegate = self
productRequest.start()
}
else {
print("Cannot perform In App Purchases.")
}
}
func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
if response.products.count != 0 {
for product in response.products {
print("\(product.localizedTitle)")
productsArray.append(product)
}
}
else {
print("There are no products.")
}
if response.invalidProductIdentifiers.count != 0 {
print("\(response.invalidProductIdentifiers.description)")
}
print("Number of products in productsArray \(productsArray.count) - Number of products in productIDs \(productIDs.count)")
}
//Payment Observer
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case SKPaymentTransactionState.Purchased:
print("Transaction completed successfully.", terminator: "")
self.deliverProduct(transaction)
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
transactionInProgress = false
case SKPaymentTransactionState.Failed:
print("Transaction Failed", terminator: "");
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
transactionInProgress = false
default:
print(transaction.transactionState.rawValue, terminator: "")
}
}
}
func deliverProduct(transaction:SKPaymentTransaction) {
if transaction.payment.productIdentifier == "com.COMPANY.NAME.BUY01"
{
print("Consumable Product 1 Purchased")
}
else if transaction.payment.productIdentifier == "com.COMPANY.NAME.BUY02"
{
print("Consumable Product 2 Purchased")
}
else if transaction.payment.productIdentifier == "com.COMPANY.NAME.BUY03"
{
print("Product 3 purchased")
}
else if transaction.payment.productIdentifier == "com.COMPANY.NAME.BUY04"
{
print("Product 4 purchased")
}
}
//Restore Purchases
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
print("Transactions Restored")
let purchasedItemIDS = []
for transaction:SKPaymentTransaction in queue.transactions {
if transaction.payment.productIdentifier == "com.COMPANY.NAME.BUY04"
{
print("Consumable Product Purchased")
}
}
let alertController = UIAlertController(title: "Restored Purchases", message: "Your purchases have been restored. ", preferredStyle: .Alert)
let OKAction = UIAlertAction(title: "OK", style: .Default) { (action) in
// ...
}
alertController.addAction(OKAction)
self.presentViewController(alertController, animated: true) {
// ...
}
}
#IBAction func exit(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
SKPaymentQueue.defaultQueue().removeTransactionObserver(self)
}
}
It sounds like you have not called finishTransaction as per the documentation here:
Transactions stay in the payment queue until they are removed.
StoreKit will call your observer’s paymentQueue: updatedTransactions:
every time that your app launches or resumes from background until
they are removed. To that effect, your customers may be repeatedly
asked to authenticate their purchases or be prevented from purchasing
your products.
Call finishTransaction: on your transaction to remove it from the
queue. Finished transactions are not recoverable. Therefore, be sure
to provide your content, to download all Apple-hosted content of a
product, or complete your purchase process before finishing your
transaction. See Finishing the Transaction for more information.
Do you know that one of the two calls to finishTransaction is actually running?