ios sandbox in-app purchase not working - ios

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

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.

Why aren't users getting in-app-purchase confirmation in Swift iOS?

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.

Why is my array out of bounds?

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

App keeps asking for Apple ID after opening view with SKPaymentTransactionObserver

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?

In-App Purchase & Restore Button : Single Product - Non-Consumable

This issue has had me going for days.
I have a simple app, that displays banners and interstitial adverts.
I am using a single view application, have main view controller (ViewController.swift) and have set up another view controller (InAppViewController.swift) to handle a pop-up page that:
Allows the user to make an in-app purchase to remove all ads (AdBanners & InterstitialAds); or
Restore purchases.
My code is error-free when I run it.
In-app purchases are running ok, but occasionally I get a sign-in request to iTunes twice.
But my restore button and associated functionality seem to be the problem.
I have set-up numerous sandbox tester accounts to test, and a new user that has not bought the app is able to restore purchases successfully. Which should not be possible, so I definitely have done something wrong here.
Here is my code:
Main View Controller:
// ViewController.swift
import UIKit
import MessageUI
import Social
import iAd
import StoreKit
class ViewController: UIViewController, MFMailComposeViewControllerDelegate, MFMessageComposeViewControllerDelegate, ADBannerViewDelegate, ADInterstitialAdDelegate
{
let defaults = NSUserDefaults.standardUserDefaults()
var product_id: NSString?;
override func viewDidLoad() {
product_id = "some.product.id";
super.viewDidLoad()
//Check if product is purchased
if (defaults.boolForKey("purchased")){
print("already purchased")
// Hide or show banner ads is purchased/not purchased.
// Advertising Banner:
self.canDisplayBannerAds = false
}
else if (!defaults.boolForKey("stonerPurchased")){
print("not yet purchased")
// Advertising Banner:
self.canDisplayBannerAds = true
}
This code seems to work perfectly. When the app loads, it is able to determine who has paid to remove ads and those who have not paid, and ad banners are shown appropriately.
It is in the second view controller (InAppPViewController.swift) That I am having problems.
here is my code:
Second View Controller - InAppViewController.swift:
// InAppPViewController.swift
import UIKit
import StoreKit
import iAd
class InAppPViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
let defaults = NSUserDefaults.standardUserDefaults()
var product_id: NSString?;
#IBOutlet weak var unlockAction: UIButton!
#IBOutlet var adBannerView: ADBannerView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func restorePurchases(sender: UIButton) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
print("Transactions Restored")
let alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
}
#IBAction func unlockAction(sender: AnyObject) {
product_id = "some.product.id";
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
//Check if product is purchased
if (defaults.boolForKey("purchased")){
}
else if (!defaults.boolForKey("stonerPurchased")){
print("false")
}
print("About to fetch the products");
// We check that we are allowed to make the purchase.
if (SKPaymentQueue.canMakePayments())
{
let productID:NSSet = NSSet(object: self.product_id!);
let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>);
productsRequest.delegate = self;
productsRequest.start();
print("Fething Products");
}else{
print("can't make purchases");
}
}
func buyProduct(product: SKProduct){
print("Sending the Payment Request to Apple");
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment);
}
//Delegate Methods for IAP
func productsRequest (request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
let count : Int = response.products.count
if (count>0) {
let validProduct: SKProduct = response.products[0] as SKProduct
if (validProduct.productIdentifier == self.product_id) {
print(validProduct.localizedTitle)
print(validProduct.localizedDescription)
print(validProduct.price)
buyProduct(validProduct);
} else {
print(validProduct.productIdentifier)
}
} else {
print("nothing")
}
}
func request(request: SKRequest, didFailWithError error: NSError) {
print("Error Fetching product information");
}
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("Received Payment Transaction Response from Apple");
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Purchased:
print("Product Purchased");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
defaults.setBool(true , forKey: "purchased")
break;
case .Failed:
print("Purchased Failed");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break;
case .Restored:
print("Already Purchased");
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
default:
break;
}
}
}
}
}
Where am I going wrong?
Questions:
Is my above code correct?
What should I modify and why?
Apologies in advance, I am new to this wonderful world of coding ... but loving every minute of it!
SKPaymentQueue.defaultQueue().addTransactionObserver(self) should be in viewDidLoad() not in restore func
You can put SKPaymentQueue.defaultQueue().restoreCompletedTransactions() like this
override func viewWillDisappear(animated: Bool) {
SKPaymentQueue.defaultQueue().removeTransactionObserver(self)
}
login twice in sandbox is normal.
hope I helped with something.
I have amended my code for the InAppPViewController.swift file as follows:
// InAppPViewController.swift
import UIKit
import StoreKit
class InAppPViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
let defaults = NSUserDefaults.standardUserDefaults()
var product_id: NSString?;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func restorePurchases(sender: UIButton) {
// Set up the observer
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
//Check if user can make payments and then proceed to restore purchase
if (SKPaymentQueue.canMakePayments()) {
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
}
#IBAction func unlockAction(sender: AnyObject) {
product_id = "some.product.id";
// Adding the observer
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
//Check if product is purchased
if (defaults.boolForKey("purchased")){
print("User already purchased this")
// Hide a view or show content depends on your requirement
}
else if (!defaults.boolForKey("Purchased")){
print("User has not yet pur hased this")
}
print("About to fetch the products");
// Check if user can make payments and then proceed to make the purchase.
if (SKPaymentQueue.canMakePayments())
{
let productID:NSSet = NSSet(object: self.product_id!);
let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>);
productsRequest.delegate = self;
productsRequest.start();
print("User can make purchases and will fetch products from Apple Store now");
}else{
print("User can't make purchases");
}
}
func buyProduct(product: SKProduct){
print("Sending the Payment Request to Apple");
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment);
}
func productsRequest (request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
let count : Int = response.products.count
if (count>0) {
let validProduct: SKProduct = response.products[0] as SKProduct
if (validProduct.productIdentifier == self.product_id) {
print(validProduct.localizedTitle)
print(validProduct.localizedDescription)
print(validProduct.price)
buyProduct(validProduct);
} else {
print(validProduct.productIdentifier)
}
} else {
print("nothing")
}
}
func request(request: SKRequest, didFailWithError error: NSError) {
print("Error Fetching product information");
}
// Allowing for all possible outcomes:
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("Received Payment Transaction Response from Apple");
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .Purchased:
print("Product Purchased")
let alert = UIAlertView(title: "Thank You", message: "Thank you for your purchase!", delegate: nil, cancelButtonTitle: "OK")
alert.show();
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
defaults.setBool(true , forKey: "purchased")
break;
case .Failed:
print("Purchased Failed");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break;
case .Restored:
print("Already Purchased");
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
break;
default:
break;
}
}
}
}
}
I left the ViewController.swift file as is.
Product purchases seem to work now.
But regarding Restore Purchases, I can run the code on my physical device, but cannot test the Restore Purchases function.
I am caught with previous Restore Purchases that are still unresolved and looping in the system. I am unable to clear my SKPaymentsQueue manually. Thus my code refuses to entertain anymore new Restore Purchase requests.

Resources