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?
Related
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.
The problem that I am experiencing is that the app keeps working if you try to cancel the subscription.
Here is my code snippet:
override func viewDidLoad() {
super.viewDidLoad()
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
let productID: NSSet = NSSet(objects: "Seb.DiPlus.RenewingSubMonthAuto", "Seb.DiPlus.RenewingSubYearAuto")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
}
#IBAction func subscribeMonth(_ sender: Any) {
for product in list {
let prodID = product.productIdentifier
if(prodID == "Seb.DiPlus.RenewingSubMonthAuto") {
p = product
buyProduct()
}
}
}
#IBAction func subscribeYear(_ sender: Any) {
for product in list {
let prodID = product.productIdentifier
if(prodID == "Seb.DiPlus.RenewingSubYearAuto") {
p = product
buyProduct()
}
}
}
#IBAction func restoreComplete(_ sender: UIButton) {
restorePurchases()
}
#IBAction func exit(_ sender: Any) {
_exit(0)
}
func restorePurchases() {
if SKPaymentQueue.canMakePayments(){
print("restored complete")
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
else{
print("restored faild, IAP not activ?")
}
}
func buyProduct() {
print("buy " + p.productIdentifier)
let pay = SKPayment(product: p)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
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)
}
}
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 "Seb.DiPlus.RenewingSubMonthAuto":
print("Subscribe Month!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
case "Seb.DiPlus.RenewingSubYearAuto":
print("Subscribe Year!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
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("ERROR: ", trans.error)
switch trans.transactionState {
case .purchased:
print("buy ok, unlock IAP HERE")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "Seb.DiPlus.RenewingSubMonthAuto":
print("Subscribe Month!!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
case "Seb.DiPlus.RenewingSubYearAuto":
print("Subscribe Year!!")
if abo < 1 {
readySubscribe()
}
abo = 1
break
default:
print("IAP not found")
}
queue.finishTransaction(trans)
case .failed:
print("buy error")
alert(title: "ERROR", message: trans.error?.localizedDescription)
queue.finishTransaction(trans)
break
default:
print("Default")
break
}
}
}
func readySubscribe() {
UserDefaults.standard.setValue(checkSubscribe, forKeyPath: "subscribe")
UserDefaults.standard.synchronize()
self.performSegue(withIdentifier: "readySubscribe", sender: self)
}
func alert (title:String, message:String?){
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: { (action) in
alert.dismiss(animated: true, completion: nil)
}))
self.present(alert, animated: true, completion: nil)
}
Once I click on restore purchases, you can continue to use the app even though the subscription is no longer activated.
So the function readySubscribe() is called.
You should not activate any purchases in paymentQueueRestoreCompletedTransactionsFinished. This function is called when the restoration process is complete. You can use it to update your UI or alert the user.
The actual restoration of products should be handled in your paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) function.
Restored transactions appear with a state of .restored. You should process them exactly as you process a .purchased state.
Since you are using auto-renewing subscription IAPs you also need to check expiration dates from the receipt and be prepared for new transactions to be presented when the subscription renews. For this reason one of the first things your app should do in didFinishLaunchingWithOptions is create a payment queue observer.
I am playing for couple days already with in-app purchases and found next issue (let me know if it expected behavior).
I have application on TestFlight at the moment, not in Apple Store.
I have a list of items that user may purchase and when user taps "Purchase" it goes through all normal steps: product is selected, then I show my AlertViewController with confirmation of purchase, and on completion of "submit" action of AlertViewController I call buyProduct in my IAP Manager . Then normal dialog about "Cancel / Buy" appears on dialog "Confirm Your In-App Purchase" with [Environment: Sandbox] etc.
However when I tap "Buy" I am redirected to the previous View Controller and "You're all set." is displayed on the previous View Controller, not the one I was at.
If it is OK for TestFlight version - then fine. If this is the behavior I will have at Apple Store then it is bad I need a fix. Here is my in-app purchase manager:
class IAPManager: NSObject {
static let sharedInstance = IAPManager()
static let IAPManagerPurchaseNotification = "IAPManagerPurchaseNotification"
fileprivate var productsRequest: SKProductsRequest?
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
func initialize() {
SKPaymentQueue.default().add(self)
}
func deinitialize() {
SKPaymentQueue.default().remove(self)
}
/// Request products from Apple Store.
/// - parameter products: IAPProduct to purchase.
func requestProduct(products: [IAPProduct], _ completionHandler: #escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
var productIdentifiers: Set<String> = Set<String>()
for product in products {
productIdentifiers.insert(product.rawValue)
}
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest?.delegate = self
productsRequest?.start()
}
/// Does actual purchasing against Apple Store.
/// - parameter product: product that user wants to purchase.
func buyProduct(_ product: SKProduct) {
Log.info(message: "User \(UsersManager.sharedInstance.currentUserId) is purchasing \(product.productIdentifier)", sender: self)
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
/// Restores previously purchased non-consumable items.
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
extension IAPManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
Log.info(message: "Loaded list of products...", sender: self)
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
Log.info(message: "Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)", sender: self)
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
Log.error(message: "Failed to load list of products with error: \(error.localizedDescription)", sender: self)
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
extension IAPManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased: complete(transaction: transaction)
case .failed: fail(transaction: transaction)
case .restored: restore(transaction: transaction)
case .deferred: Log.info(message: "Purchase is deffered", sender: self)
case .purchasing: Log.info(message: "Purchase is in progress", sender: self)
}
}
}
private func complete(transaction: SKPaymentTransaction) {
Log.info(message: "Purchase is completed.", sender: self)
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else {
return
}
Log.info(message: "Purchase \(productIdentifier) is in restore state.", sender: self)
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
Log.info(message: "Purchase is failed.", sender: self)
if let transactionError = transaction.error as NSError? {
if transactionError.code != SKError.paymentCancelled.rawValue {
Log.error(message: "Transaction Error: \(transactionError.localizedDescription)", sender: self)
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else {
return
}
NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPManager.IAPManagerPurchaseNotification), object: identifier)
}
}
Code that calls IAPManager:
let alert = UIAlertController(title: "<Title>", message: "<Message>", preferredStyle: .alert)
IAPManager.sharedInstance.requestProduct(products: [product]) { success, products in
guard success, let products = products, !products.isEmpty else {
Log.error(message: "Error occured during product request.", sender: self)
return
}
let product = products[0]
let submit = UIAlertAction(title: "Ok", style: .default) { action in
IAPManager.sharedInstance.buyProduct(product)
}
let cancel = UIAlertAction(title: "No", style: .destructive, handler: nil)
alert.addAction(submit)
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
Issue disappeared on its own. I was fixing multithreading and probably that's was the cause. I cannot reproduce right now, but if I will find the real cause - I'll post it here. Thanks all.
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.
I am using IAPHelper to implement in-app purchase, an error is occurring when I go to another view controller after pressing the purchase button. For instance, when I press the purchase button then go to another view controller the error is presented after finishing the purchase work.
class selectQuestion_ViewController: UITableViewController, UITableViewDelegate, UITableViewDataSource{
let helper = IAPHelper(productIdentifiers: NSSet(object: "sppid") as Set<NSObject>)
func purchase(sender: AnyObject)
{
startLoading("Purchasing ..")
self.helper.requestProductsWithCompletionHandler({ (success, products) -> Void in
if success {
self.endLoading()
println("wohooooo")
var sdad = self.helper.productsDict["sppid"]
self.helper.buyProduct(sdad!)
} else {
self.endLoading()
let alert = UIAlertController(title: "Error", message: "Cannot retrieve products list right now.", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
})
}
I recommend you do not use such modules. It would be much better for you and your understanding of IAPs that you check this tutorial and implement the code and methods described there: https://www.youtube.com/watch?v=h1gQklbrgjc
Once you have implemented that code, if something doesn't work (you may have forgotten a line somewhere), come back to SO and we will gladly help you out. Already, as quick help, here is an answer that could help you with most errors once you implement your IAP code with something else than IAP helper: My IAP isn't working. Bugs at func Paymentqueue
In fact, you can watch the video and copy paste the code of my question. My IAP code works ;) Will save you time. But watch the video to understand how to implement IAPs !
I hope this answer helped you ;)
Here is the full IAP code :
import UIKit
import StoreKit
class GameViewController: UIViewController, ADBannerViewDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver, GKGameCenterControllerDelegate,GADBannerViewDelegate{
#IBOutlet var outRemoveAds: UIButton!
#IBOutlet var outRestorePurchases: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if NSUserDefaults.standardUserDefaults().objectForKey("val") != nil {
print("Has a value.")
banner.removeFromSuperview()
bannerGoogle.removeFromSuperview()
outRemoveAds.removeFromSuperview()
outRestorePurchases.removeFromSuperview()
removeInterFrom = 1
}
else {
print("No Value.")
}
if(SKPaymentQueue.canMakePayments()){
print("IAP is enabled, loading...")
let productID:NSSet = NSSet(objects:"IAP id")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
}
else{
print("Please enable IAPS")
}
}
//IAP Ads
#IBAction func removeAds(sender: UIButton) {
for product in list{
let prodID = product.productIdentifier
if (prodID == "IAP id"){
p = product
buyProduct()
break
}
}
}
#IBAction func restorePurchases(sender: UIButton) {
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
//IAP Functions
var list = [SKProduct]()
var p = SKProduct()
func removeAds(){
banner.removeFromSuperview()
bannerGoogle.removeFromSuperview()
outRemoveAds.removeFromSuperview()
outRestorePurchases.removeFromSuperview()
let theValue = 10
NSUserDefaults.standardUserDefaults().setObject(theValue, forKey: "val")
NSUserDefaults.standardUserDefaults().synchronize()
}
func buyProduct(){
print("Buy: "+p.productIdentifier)
let pay = SKPayment (product: p)
SKPaymentQueue.defaultQueue().addPayment(pay as SKPayment)
}
func productsRequest(request: SKProductsRequest, didReceiveResponse 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 as SKProduct)
}
}
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("IAP unlocked")
print(p.productIdentifier)
let prodID = p.productIdentifier as String
switch prodID{
case "IAP id":
print("Remove Ads")
removeAds()
default:
print("IAP not setup")
}
queue.finishTransaction(trans)
break
case .Failed:
print ("Buy error")
queue.finishTransaction(trans)
break
default:
print("default: Error")
break
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
print("Purchases Restored")
_ = []
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction as SKPaymentTransaction
let prodID = t.payment.productIdentifier as String
switch prodID{
case "IAP id":
print("Remove Ads")
removeAds()
default:
print("IAP not setup")
}
}
}
func finishTransaction(trans:SKPaymentTransaction){
print("Finshed Transaction")
}
func paymentQueue(queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
print("Remove Transaction")
}
}
Put:
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
in viewDidLoad or viewDidAppear