Unable to purchase IAP as sandbox user - ios

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:
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.
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.
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.

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)")
}
}

Related

In app purchase products are returns '0' products by SKProductsResponse

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.

In App Purchase issue with SKProduct() in Swift

I am trying to implement In-App purchases into an app I am creating however when I try to 'get' the product I have var products = [SKProducts]() however this returns an empty array causing the application to crash.
I have check off all the tax agreements etc. and when I test it out in apples in app purchase example project the IAPs show up.
The full code where the problem occurs is below.
class Model {
var products = [SKProduct]()
func getProduct(containing keyword: String) -> SKProduct? {
// print("The array of SKProducts in Model getProduct is \(products)")
// let test = products.filter { $0.productIdentifier.contains(keyword) }.first
print("The products are: \(products)")
print(products.filter { $0.productIdentifier.contains(keyword) }.first)
return products.filter { $0.productIdentifier.contains(keyword) }.first
}
}
The print statements return:
"The products are: []" and
"nil"
If it helps the full project can be found on GitHub here
You function getProduct does nothing else than to print an empty array (products), and to filter this empty array and retrieve the first element (which does not exist).
I suggest to use a helper like this (I copied it from somewhere, don't remember where):
import Foundation
import StoreKit
class IAPHelper: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
typealias RequestProductsCompletionHandler = (_ success:Bool, _ products:[SKProduct]?) -> ()
var completionHandler: RequestProductsCompletionHandler?
var productsRequest: SKProductsRequest?
var productIdentifiers: Set<String>
var purchasedProductIdentifiers: Set<String>?
// MARK: - Init
init(identifiers: Set<String>) {
// Store product identifiers
productIdentifiers = identifiers;
// Check for previously purchased products
purchasedProductIdentifiers = Set()
for productIdentifier in productIdentifiers {
let productPurchased = UserDefaults.standard.bool(forKey: productIdentifier)
if productPurchased {
purchasedProductIdentifiers?.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)");
} else {
print("Not purchased: \(productIdentifier)");
}
}
super.init() // must be called after subclass init, but before "self" is used
SKPaymentQueue.default().add(self)
} // init
func requestProductsWithCompletionHandler(_ completionHandler: #escaping RequestProductsCompletionHandler) {
self.completionHandler = completionHandler
productsRequest = SKProductsRequest.init(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
// MARK: - Products request delegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
productsRequest = nil
let skProducts = response.products
for skProduct in skProducts {
print("Found product: \(skProduct.productIdentifier), \(skProduct.localizedTitle), \(skProduct.price.floatValue)");
}
completionHandler!(true, skProducts)
completionHandler = nil
}
func request(_ request: SKRequest, didFailWithError error: Error) {
// NOTE: If this happens on the simulator, close the simulator window and re-run the app. This helps normally !!!
print("Failed to load list of products. Error: \(error)")
productsRequest = nil
completionHandler!(false, nil)
completionHandler = nil
}
// MARK: - Purchase
func productPurchchased(_ productIdentifier:String) -> Bool {
return purchasedProductIdentifiers!.contains(productIdentifier)
}
func buyProduct(_ product:SKProduct) {
print("Buying \(product.productIdentifier)");
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case SKPaymentTransactionState.purchased:
self.completeTransaction(transaction)
case SKPaymentTransactionState.failed:
self.failedTransaction(transaction)
case SKPaymentTransactionState.restored:
self.restoreTransaction(transaction)
default:
print("Error: Unknown SKPaymentTransactionState")
}
}
}
func completeTransaction(_ transaction:SKPaymentTransaction) {
print("completeTransaction...");
self.provideContentForProductIdentifier(transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
func restoreTransaction(_ transaction:SKPaymentTransaction) {
print("restoreTransaction...");
self.provideContentForProductIdentifier(transaction.original!.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
func failedTransaction(_ transaction:SKPaymentTransaction) {
print("failedTransaction...");
if let error = transaction.error as NSError?, error.code == SKError.paymentCancelled.rawValue {
print("Transaction error:\(error.localizedDescription)");
}
SKPaymentQueue.default().finishTransaction(transaction)
}
func provideContentForProductIdentifier(_ productIdentifier:String) {
purchasedProductIdentifiers?.insert(productIdentifier)
UserDefaults.standard.set(true, forKey:productIdentifier)
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperProductPurchasedNotification), object: productIdentifier, userInfo: nil)
}
func restoreCompletedTransactions() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
To get products you have to make SKProductsRequest and wait for the answer in SKProductsRequestDelegate methods.
Create and start request:
let request = SKProductsRequest(productIdentifiers: ids)
request.delegate = self // self is delegate of SKProductsRequestDelegate, see below
request.start()
Where ids has to be Set<String> of you product identifiers.
Conform to SKProductsRequestDelegate:
extension MyClass: SKProductsRequestDelegate {
internal func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print(response.products) // here is your [SKProduct] array
print(response.invalidProductIdentifiers) // if you put wrong ids they would be here
}
internal func request(_ request: SKRequest, didFailWithError error: Error) {
print(error) // something wrong
}
}

How to handle redeeming a consumable Promo Code for the App Store

I have searched everywhere for this but haven't found the answer to my problem. This is the scenario:
I have a game live in the App Store. I have consumable IAPs for x number of Coins. I generated a Promo Code for a user to have some free Coins. He redeemed the code successfully in the App Store, but when he opened my game his Coins weren't credited. He tried tapping the 'Restore' button, restarting his device, and logging out and then back in again with his Apple ID, but nothing worked.
I tried it myself with another consumable Promo Code and couldn't get it to work either. I successfully redeemed a non-consumable IAP however.
So my question is, do I have to handle a consumable Promo Code being used in my code? Otherwise, how can users redeem consumable Promo Codes?
Edit: showing my code
GameStoreViewController.swift (to select an IAP to buy from a table view)
#objc func tableView(_ tableView: UITableView!, didSelectRowAtIndexPath indexPath: IndexPath!) {
// First, make sure the user is authorised to make payments on his/her device
if IAPHelper.canMakePayments() {
// Then, make sure the product has been retrieved and placed in the _coinProducts array. If not, show alert
if _coinProducts.count > 0 {
activitySpinnerStart()
print("row \((indexPath as NSIndexPath).row) tapped")
let product = _coinProducts[(indexPath as NSIndexPath).row]
IAPList.store.buyProduct(product) // Purchasing the product. Fires productPurchased(notification:)
} else {
showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))
}
} else {
showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoIAPAllowedMessage"))
}
}
IAPHelper.swift:
import StoreKit
/// Notification that is generated when a product is purchased.
public let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
/// Notification that is generated when a transaction fails.
public let IAPHelperTransactionFailedNotification = "IAPHelperTransactionFailedNotification"
/// Notification that is generated when cannot retrieve IAPs from iTunes.
public let IAPHelperConnectionErrorNotification = "IAPHelperConnectionErrorNotification"
/// Notification that is generated when we need to stop the spinner.
public let IAPHelperStopSpinnerNotification = "IAPHelperStopSpinnerNotification"
/// Product identifiers are unique strings registered on the app store.
public typealias ProductIdentifier = String
/// Completion handler called when products are fetched.
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()
/// A Helper class for In-App-Purchases.
open class IAPHelper : NSObject {
/// MARK: - User facing API
fileprivate let productIdentifiers: Set<ProductIdentifier>
fileprivate var purchasedProductIdentifiers = Set<ProductIdentifier>()
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
// These two notifications are private, so are needed in addition to the public ones on line 12
//static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
//static let IAPHelperTransactionFailedNotification = "IAPHelperTransactionFailedNotification"
/// Initialize the helper. Pass in the set of ProductIdentifiers supported by the app.
public init(productIds: Set<ProductIdentifier>) {
self.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()
}
// Adding functions from SA
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("Restore queue finished.")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)
}
public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
print("Restore queue failed.")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)
}
}
// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let products = response.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)
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)
clearRequestAndHandler()
}
fileprivate 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:
completeTransaction(transaction)
break
case .failed:
failedTransaction(transaction)
break
case .restored:
restoreTransaction(transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
fileprivate func completeTransaction(_ transaction: SKPaymentTransaction) {
print("completeTransaction...")
deliverPurchaseNotificationForIdentifier(transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
fileprivate func restoreTransaction(_ transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restoreTransaction... \(productIdentifier)")
deliverPurchaseNotificationForIdentifier(productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
fileprivate func failedTransaction(_ transaction: SKPaymentTransaction) {
print("failedTransaction...")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)
if transaction.error!._code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(String(describing: transaction.error?.localizedDescription))")
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)
} else {
print("Transaction Error else statement")
}
SKPaymentQueue.default().finishTransaction(transaction)
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)
}
fileprivate func deliverPurchaseNotificationForIdentifier(_ identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperPurchaseNotification), object: identifier)
}
}
IAPList.swift:
import Foundation
// Use enum as a simple namespace. (It has no cases so you can't instantiate it.)
public struct IAPList {
/// TODO: Change this to whatever you set on iTunes connect
fileprivate static let Prefix = "com.xxx.xxx."
/// MARK: - Supported Product Identifiers
/// List your IAPs here:
public static let PackOf4000Coins = Prefix + "4000Coins"
public static let PackOf10000Coins = Prefix + "10000Coins"
public static let PackOf30000Coins = Prefix + "30000Coins"
public static let PackOf75000Coins = Prefix + "75000Coins"
public static let PackOf175000Coins = Prefix + "175000Coins"
public static let RemoveAds = Prefix + "RemoveAds"
public static let PlayerEditor = Prefix + "PlayerEditor"
// All of the products assembled into a set of product identifiers.
fileprivate static let productIdentifiers: Set<ProductIdentifier> =
[IAPList.PackOf4000Coins, IAPList.PackOf10000Coins, IAPList.PackOf30000Coins,
IAPList.PackOf75000Coins, PackOf175000Coins, RemoveAds, PlayerEditor]
/// Static instance of IAPHelper.
public static let store = IAPHelper(productIds: IAPList.productIdentifiers)
}
/// Return the resource name for the product identifier.
func resourceNameForProductIdentifier(_ productIdentifier: String) -> String? {
return productIdentifier.components(separatedBy: ".").last
}

SKProductsRequest returning 0 Products

I'm trying to do IAP however for some reason, my SKProductsRequest returns 0 products.
-Test products have been added to iTunes connect properly
-Banking and Taxing information is filled
-The product's bundle id matches the app bundle id
-I've waited up to two days for it to get processed through the servers
I used this youtube tutorial to build the app:
https://www.youtube.com/watch?v=zRrs7O5yjKI
And here is the code:
import UIKit
import StoreKit
class ViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
#IBOutlet weak var lblAd: UILabel!
#IBOutlet weak var lblCoinAmount: UILabel!
#IBOutlet weak var outRemoveAds: UIButton!
#IBOutlet weak var outAddCoins: UIButton!
#IBOutlet weak var outRestorePurchases: UIButton!
var coins = 50
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
outRemoveAds.isEnabled = false
outAddCoins.isEnabled = false
outRestorePurchases.isEnabled = false
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
let productID: NSSet = NSSet(objects: "com.IAPTesters.10Dolla", "com.IAPTesters.RemoveAds")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
}
#IBAction func btnRemoveAds(_ sender: Any) {
print("rem ads")
for product in list {
let prodID = product.productIdentifier
if(prodID == "com.IAPTesters.RemoveAds") {
p = product
buyProduct()
}
}
}
#IBAction func btnAddCoins(_ sender: Any) {
for product in list {
let prodID = product.productIdentifier
if(prodID == "com.IAPTesters.10Dolla") {
p = product
buyProduct()
}
}
}
#IBAction func btnRestorePurchases(_ sender: Any) {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
func buyProduct() {
print("buy " + p.productIdentifier)
let pay = SKPayment(product: p)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
func removeAds() {
lblAd.removeFromSuperview()
}
func addCoins() {
coins += 50
lblCoinAmount.text = "\(coins)"
}
var list = [SKProduct]()
var p = SKProduct()
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request")
let myProduct = response.products
for product in myProduct {
print("product added")
print(product.productIdentifier)
print(product.localizedTitle)
print(product.localizedDescription)
print(product.price)
list.append(product)
}
outRemoveAds.isEnabled = true
outAddCoins.isEnabled = true
outRestorePurchases.isEnabled = true
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("transactions restored")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "com.IAPTesters.RemoveAds":
print("remove ads")
removeAds()
case "com.IAPTesters.10Dolla":
print("add coins to account")
addCoins()
default:
print("IAP not found")
}
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("add payment")
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
print(trans.error)
switch trans.transactionState {
case .purchased:
print("buy ok, unlock IAP HERE")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "com.IAPTesters.RemoveAds":
print("remove ads")
removeAds()
case "com.IAPTesters.10Dolla":
print("add coins to account")
addCoins()
default:
print("IAP not found")
}
queue.finishTransaction(trans)
case .failed:
print("buy error")
queue.finishTransaction(trans)
break
default:
print("Default")
break
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
When you run the app it will print "IAP is enabled, loading" and than "product request" but nothing else.
If I print response.invalidProductIdentifiers in the productsRequest function it will return my products: ["com.IAPTesters.RemoveAds", "com.IAPTesters.10Dolla"]
Thanks in advance for the help
It turns out that my banking and taxing information was filled out incorrectly. I refilled it and than I had to wait about 30 minutes for it to work again. Everything is now working correctly! Thanks for all the help
If you refer to the SKProductsRequest documentation you will see:
Note
Be sure to keep a strong reference to the request object; otherwise, the system might deallocate the request before it can complete.
Your SKProductsRequest instance is a local constant in viewDidLoad. This will be deallocated as soon as viewDidLoad exits and since the product request will complete asynchronously, this will be before the request is completed and you will therefore never get a call back.
You should retain your SKProductsRequest in a property so that it isn't released.
class ViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
#IBOutlet weak var lblAd: UILabel!
#IBOutlet weak var lblCoinAmount: UILabel!
#IBOutlet weak var outRemoveAds: UIButton!
#IBOutlet weak var outAddCoins: UIButton!
#IBOutlet weak var outRestorePurchases: UIButton!
var productsRequest: SKProductsRequest?
var coins = 50
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
outRemoveAds.isEnabled = false
outAddCoins.isEnabled = false
outRestorePurchases.isEnabled = false
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
let productID: NSSet = NSSet(objects: "com.IAPTesters.10Dolla", "com.IAPTesters.RemoveAds")
self.productsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request?.delegate = self
request?.start()
} else {
print("please enable IAPS")
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("product request")
let myProduct = response.products
for product in myProduct {
print("product added")
print(product.productIdentifier)
print(product.localizedTitle)
print(product.localizedDescription)
print(product.price)
list.append(product)
}
outRemoveAds.isEnabled = true
outAddCoins.isEnabled = true
outRestorePurchases.isEnabled = true
self.productsRequest = nil
}
FYI, your implementation of paymentQueueRestoreCompletedTransactionsFinished is incorrect; you should process the restored transactions in updatedTransactions through the .restored transaction state. The paymentQueueRestoreCompletedTransactionsFinished method should only be used to update your UI or perform any other tasks that are required when the restoration process is complete.
If I 'm understood correctly then the problem with your implementation is you have created your products in iTunes with wrong identifiers. As I know the product identifiers should be like your application bundle identifier followed by your product functionality i.e, COM.COMPANYNAME.MYNEWAPPLICATION.REMOVEADS, COM.COMPANYNAME.MYNEWAPPLICATION.10DOLLA
Sample code be like,
typealias SSIAPHelperCompletion = (_ result: [SKProduct]?, _ error: Error?) ->Void
class SSIAPHelper: NSObject {
fileprivate let productsRequest = SKProductsRequest(productIdentifiers: Set(arrayLiteral: "com.companyname.appname.removeads","com.companyname.appname.10dolla"))
fileprivate var completion: SSIAPHelperCompletion?
static let shared = SSIAPHelper()
func requestProducts(completionHandler: #escaping SSIAPHelperCompletion) {
self.completion = completionHandler
self.productsRequest.delegate = SSIAPHelper.shared
self.productsRequest.start()
}
}
extension SSIAPHelper: SKProductsRequestDelegate{
func requestDidFinish(_ request: SKRequest) {
print(#function)
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print(#function, response.products)
if let lCompletion = self.completion{
lCompletion(response.products, nil)
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print(#function)
}
}
You can get products by simply calling below function from your viewDidLoad()
SSIAPHelper.shared.requestProducts(completionHandler: { (result, error) in
})

iOS in-app purchases randomly not being recognized, or never recognized to begin with

Ok this one really has me stumped and if anyone can figure it out I will love you long time. I have several iOS apps and I'm getting a lot of reports from users that when they pay using apple's in-app payment system to purchase app upgrades, the apps are not recognizing their purchase, or sometimes it recognizes it for a day and then it stops working.
The weird thing is I have tested it on my iPhone and iPad, both in sandbox and after publishing with a real account, and I never have these issues. I thought maybe it was an issue with the new iOS 11 but even users on iOS 10 and 9 are reporting this issue. For some users, uninstalling the apps and downloading again from app store, then clicking restore purchases button fixes it, but for most user's it doesn't.
I have tried everything apple recommends, such as telling users to make sure they are using the correct apple ID, making sure the device is set to allows in-app payments, etc but none of those solutions apply or work.
So without further ado, here's my code
SharedDefaults
fileprivate enum SUDK: String {
case updatedToProVersion = "UDK_UPDATED_TO_PRO_VERSION"
}
public class SharedDefaults: NSObject {
public static var shared: SharedDefaults!
private let defaults: UserDefaults
public init(groupIdentifier: String) {
if let userDefaults = UserDefaults(suiteName: groupIdentifier) {
defaults = userDefaults
} else {
defaults = UserDefaults.standard
assert(false, "Defaults could not be created with the given app group - \(groupIdentifier)")
}
super.init()
}
public static var isPremiumVersionActive: Bool {
set {
shared.defaults.set(newValue, forKey: SUDK.updatedToProVersion.rawValue)
}
get {
//// return true to set pro version to always on. This is the in-app purchase the user can make to upgrade the app.
////return true
return shared.defaults.bool(forKey: SUDK.updatedToProVersion.rawValue)
}
}
Upgrade Screen and Purchase Handlers
import UIKit
import StoreKit
class UpgradeNowController: UITableViewController {
//MARK: Constants
struct CellIdentifiers {
static let buttonCell = "UpgradeNowButtonCell"
static let discountedButtonCell = "UpgradeNowDiscountedButtonCell"
static let buttonOnlyCell = "UpgradeNowButtonOnlyCell"
static let detailCell = "UpgradeNowDetailCell"
}
//MARK: Variables
var allProducts = [SKProduct]()
var products = [SKProduct]()
public var activeProductID: String!
var detailItems: [(title: String, detail: String, image: String?)] = []
//MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
activeProductID = RageProducts.proversionupgrade
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
let restoreButton = UIBarButtonItem(title: "Restore",
style: .plain,
target: self,
action: #selector(UpgradeNowController.restoreTapped(_:)))
navigationItem.rightBarButtonItem = restoreButton
NotificationCenter.default.addObserver(self, selector: #selector(UpgradeNowController.handlePurchaseNotification(_:)),
name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification),
object: nil)
configureTableView()
reloadTableView()
configureDetailItems()
}
deinit {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: nil)
}
func reload() {
products = []
tableView.reloadData()
RageProducts.store.requestProducts{ [weak self] success, products in
if success {
self?.allProducts = products!
self?.products = products!.filter { $0.productIdentifier == self?.activeProductID }
self?.tableView.reloadData()
}
self?.refreshControl?.endRefreshing()
}
}
func restoreTapped(_ sender: AnyObject) {
RageProducts.store.restorePurchases()
}
func handlePurchaseNotification(_ notification: Notification) {
guard let productID = notification.object as? String else { return }
for (index, product) in products.enumerated() {
guard product.productIdentifier == productID else { continue }
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .fade)
}
SharedDefaults.isPremiumVersionActive = true
}
//MARK: IAPPopupViewControllerDelegate
extension UpgradeNowController: IAPPopupViewControllerDelegate {
func iapPopupViewController(_ viewController: IAPPopupViewController, didTapCancelForProduct product: SKProduct) {
viewController.dismiss(animated: true)
}
func iapPopupViewController(_ viewController: IAPPopupViewController, didTapPurchaseForProduct product: SKProduct) {
viewController.dismiss(animated: true)
RageProducts.store.buyProduct(product)
}
}
IAPHelper
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: \(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)
}
}
Note: lots of code has been stripped I've only included relevant code to the in app purchases. Maybe someone can look at it and let me know if I'm doing anything wrong but as far as I can see and research I've followed all guidelines for storekit and in app purchases as best as possible.

Resources