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
}
}
Related
I have implemented in app purchases for auto renewable subscriptions in my app, but I am trying to implement something once a purchased has been completed.
Below is my SubscriptionService for the IAPs
class SubscriptionService: NSObject {
static let sessionIdSetNotification = Notification.Name("SubscriptionServiceSessionIdSetNotification")
static let optionsLoadedNotification = Notification.Name("SubscriptionServiceOptionsLoadedNotification")
static let restoreSuccessfulNotification = Notification.Name("SubscriptionServiceRestoreSuccessfulNotification")
static let purchaseSuccessfulNotification = Notification.Name("SubscriptionServiceRestoreSuccessfulNotification")
static let shared = SubscriptionService()
private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
var productDidPurchased: (() -> Void)?
var hasReceiptData: Bool {
return loadReceipt() != nil
}
var options: [Subscription]? {
didSet {
NotificationCenter.default.post(name: SubscriptionService.optionsLoadedNotification, object: options)
}
}
func loadSubscriptionOptions() {
let products: Set = ["productIDS"]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
}
func uploadReceipt(completion: ((_ success: Bool) -> Void)? = nil) {
if let receiptData = loadReceipt() {
}
}
private func loadReceipt() -> Data? {
guard let url = Bundle.main.appStoreReceiptURL else {
return nil
}
do {
let data = try Data(contentsOf: url)
return data
} catch {
print("Error loading receipt data: \(error.localizedDescription)")
return nil
}
}
func purchase(subscription: Subscription) {
let payment = SKPayment(product: subscription.product)
print("Product being bought: \(payment)")
SKPaymentQueue.default().add(payment)
}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
extension SubscriptionService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
options = response.products.map { Subscription(product: $0) }
print("in here 1")
print(options!)
// let purchased = UserDefaults.standard.bool(forKey: "com.mylawnow.sub.allaccess")
//print("Purchased\(purchased)")
print("in here 2")
}
func request(_ request: SKRequest, didFailWithError error: Error) {
if request is SKProductsRequest {
print("Subscription Options Failed Loading: \(error.localizedDescription)")
}
}
}
//-------------------------------------------------------------
This is the part that does not seem to be working. None of the functions seem to be firing (I have implemented print statements to see if they are ever hit, but they don't seem to be.)
extension SubscriptionService: SKPaymentTransactionObserver{
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("updating")
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
print("purchased made")
complete(transaction: transaction)
break
case .failed:
print("purchased failed")
fail(transaction: transaction)
break
case .restored:
print("restored")
restore(transaction: transaction)
break
case .deferred:
print("purchase deferred")
break
case .purchasing:
print("purchase being made")
break
}
}
}
I am assuming that the complete function should be used to do certain functionality once the purchased is made.
private func complete(transaction: SKPaymentTransaction) {
print("completed...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
deliverPurchaseNotificationFor(identifier: productIdentifier)
print("identifier: \(productIdentifier)")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("failed...")
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
print("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)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}
So I found the error. In my loadSubscriptions function, I needed to app an observer to the SKPaymentQueue. So it should be like below:
func loadSubscriptionOptions() {
let products: Set = ["productIDS"]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
SKPaymentQueue.default().add(self) //--> this line is needed
}
I am having trouble trying to follow Apple's recommendation of adding a transaction queue observer in didFinishLaunchingWithOptions.
Specifically I am trying to adapt code from a Ray Wenderlich tutorial which does not do this - it adds the observer only once the 'buy' button is tapped.
My app crashes when the buyProduct function is called:
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
In my log I can see that the init of IAPHelper is called twice, thus calling SKPaymentQueue.default().add(self) twice. I am sure that's the problem but I'm confused how to fix it.
Here is my code...
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
IAPHelper.sharedInstance = IAPHelper() // creates the singleton for IAPHelper
// other code here //
return true
}
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]?) -> ()
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?
static var sharedInstance = IAPHelper() // singleton
override init() {
// Set up the list of productIdentifiers
let PackOf4000Coins = "com.xxx.xxx.4000Coins"
let PackOf10000Coins = "com.xxx.xxx.10000Coins"
let PackOf30000Coins = "com.xxx.xxx.30000Coins"
let PackOf75000Coins = "com.xxx.xxx.75000Coins"
let PackOf175000Coins = "com.xxx.xxx.175000Coins"
let PackOf750000Coins = "com.xxx.xxx.750000Coins"
let RemoveAds = "com.xxx.xxx.RemoveAds"
let PlayerEditor = "com.xxx.xxx.PlayerEditor"
self.productIdentifiers = [PackOf4000Coins, PackOf10000Coins, PackOf30000Coins, PackOf75000Coins, PackOf175000Coins, PackOf750000Coins, RemoveAds, PlayerEditor]
for productIdentifier in self.productIdentifiers {
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()
}
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)
}
}
GameStoreViewController.swift (relevant code only):
#objc func tableView(_ tableView: UITableView!, didSelectRowAtIndexPath indexPath: IndexPath!) {
if IAPHelper.canMakePayments() {
activitySpinnerStart()
let product = _coinProducts[(indexPath as NSIndexPath).row]
IAPHelper.sharedInstance.buyProduct(product) // Purchasing the product. Fires productPurchased(notification:)
} else {
showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoIAPAllowedMessage"))
}
}
In the end I used SwiftyStoreKit which solved the problem for me. I highly recommend it.
Edit to show SwiftyStoreKit.completeTransactions which goes in didFinishLaunchingWithOptions() in appDelegate. unlockIAPContent() is where I put the purchase logic and this will handle promo codes and unfinished transactions:
// This registers the transaction observer and listens for unfinished transactions
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
// Unlock content
self.unlockIAPContent(productID: purchase.productId)
if purchase.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
case .failed, .purchasing, .deferred:
break // do nothing
}
}
}
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
}
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)")
}
}
I'm trying to implement that new paymentQueue(_:shouldAddStorePayment:for:) method so my app can handle IAPs directly from the App Store.
I'm using the itms-services:// url to test it, like it says here.
The thing is, my SKPaymentTransactionObserver is a specific view controller, and if it's not visible when I open the itms-services:// link the delegate method won't be called.
What can I do about that? I think I'd have to detect if the user is coming from the App Store to push the right view controller, but I don't know how. The only other option I can think of right now is to make the App Delegate an SKPaymentTransactionObserver, but that seems really cumbersome and I couldn't get it to work when I tried it. Is there any other way?
Here's a class I did that can help you achieve what you want, simply copy the code below and paste it inside a new file and then you can simply access the class StoreManager.shared to whatever method/variable you want to access.
1- To init this class, just call from your didFinishLaunchingWithOptions
StoreManager.shared.Begin() and then payment observer is added.
import Foundation
import StoreKit
class StoreManager: NSObject{
/**
Initialize StoreManager and load subscriptions SKProducts from Store
*/
static let shared = StoreManager()
func Begin() {
print("StoreManager initialized"))
}
override init() {
super.init()
// Add pyament observer to payment qu
SKPaymentQueue.default().add(self)
}
func requestProductWithID(identifers:Set<String>){
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers:
identifers)
request.delegate = self
request.start()
} else {
print("ERROR: Store Not Available")
}
}
func buyProduct(product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK:
// MARK: SKProductsRequestDelegate
//The delegate receives the product information that the request was interested in.
extension StoreManager:SKProductsRequestDelegate{
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
var products = response.products as [SKProduct]
var buys = [SKProduct]()
if (products.count > 0) {
for i in 0 ..< products.count {
let product = products[i]
print("Product Found: ",product.localizedTitle)
}
} else {
print("No products found")
}
let productsInvalidIds = response.invalidProductIdentifiers
for product in productsInvalidIds {
print("Product not found: \(product)")
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Something went wrong: \(error.localizedDescription)")
}
}
// MARK:
// MARK: SKTransactions
extension StoreManager: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
completeTransaction(transaction: transaction)
break
case .failed:
failedTransaction(transaction: transaction)
break
case .restored:
restoreTransaction(transaction: transaction)
break
case .deferred:
// TODO show user that is waiting for approval
break
case .purchasing:
break
}
}
}
private func completeTransaction(transaction: SKPaymentTransaction) {
print("completeTransaction...")
deliverPurchaseForIdentifier(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restoreTransaction(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restoreTransaction... \(productIdentifier)")
deliverPurchaseForIdentifier(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func failedTransaction(transaction: SKPaymentTransaction) {
if let error = transaction.error as NSError? {
if error.domain == SKErrorDomain {
// handle all possible errors
switch (error.code) {
case SKError.unknown.rawValue:
print("Unknown error")
case SKError.clientInvalid.rawValue:
print("client is not allowed to issue the request")
case SKError.paymentCancelled.rawValue:
print("user cancelled the request")
case SKError.paymentInvalid.rawValue:
print("purchase identifier was invalid")
case SKError.paymentNotAllowed.rawValue:
print("this device is not allowed to make the payment")
default:
break;
}
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseForIdentifier(identifier: String?) {
guard let identifier = identifier else { return }
}
}
//In-App Purchases App Store
extension StoreManager{
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
return true
//To hold
//return false
//And then to continue
//SKPaymentQueue.default().add(savedPayment)
}
}