I am trying to implement in app purchases in a Swift app, of a consumable in-app purchase, with product ID productID
Research
Using this answer, I learned about the basic pattern for in app purchases, and tried to adapt it, like so. I do not need to use the NSUserDefaults method of having the app remember if a purchase is made, as I am handling that with a different solution.
Code
class ViewController: UICollectionViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
viewDidLoad() {
//Unimportant stuff left out
product_id = "productID"
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
}
#IBAction func thisISTheButtonToTriggerTheInAppPurchases(sender: AnyObject) {
if (SKPaymentQueue.canMakePayments()) {
var productID:NSSet = NSSet(object: self.product_id!);
var productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>);
productsRequest.delegate = self;
productsRequest.start();
print("Fetching Products");
}else{
print("can't make purchases");
}
}
func buyProduct(product: SKProduct){
print("Sending the Payment Request to Apple");
var payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment);
}
func productsRequest (request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
var count : Int = response.products.count
if (count>0) {
var validProducts = response.products
var 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: [AnyObject]!) {
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)
break;
case .Failed:
print("Purchased Failed");
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
break;
case .Restored:
print("Already Purchased");
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
default:
break;
}
}
}
}
}
Error
Type 'ViewController' does not conform to protocol 'SKPaymentTransactionObserver'
I researched this error a bit, and sources like this suggest that such an error is due to not having required functions.
According to Apple, paymentQueue:updatedTransactions: is the only one required, but I seem to be implementing that. A possible theory of mine is that I am not implementing it correctly - if true, how do I fix that?
Expected Behavior
The app presents, and if given favorable user input, performs the IAP with the ID productID.
Thanks for helping, and let me know if you need any more information!
It's very simple. The error is about SKPaymentTransactionObserver. Its only required method is:
func paymentQueue(queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction])
The signature for your method doesn't match that:
func paymentQueue(queue: SKPaymentQueue!,
updatedTransactions transactions: [AnyObject]!)
...so it isn't the same method, so you don't conform.
Related
I have no problem when I click on restore purchase. But when I click on unlockProButt or buy100CoinsButt, I get the error "Thread 1: Fatal error: Index out of range". What is the problem? How can I make the purchase smoothly? I have always received errors in all the in-app purchase examples I have tried. I always have problems with this line of code.
Error Line: purchaseMyProduct(validProducts[productIndex])
Error: Thread 1: Fatal error: Index out of range
import UIKit
import StoreKit
class ViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
#IBOutlet weak var buy100coinsButton: UIButton!
#IBOutlet weak var unlockProButton: UIButton!
#IBOutlet weak var restorePurchaseButton: UIButton!
var productsRequest = SKProductsRequest()
var validProducts = [SKProduct]()
var productIndex = 0
// viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
buy100coinsButton.isHidden = true
unlockProButton.isHidden = true
fetchAvailableProducts()
}
func fetchAvailableProducts() {
let productIdentifiers = NSSet(objects:
"..", // 0
"..." // 1
)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
if (response.products.count > 0) {
validProducts = response.products
// 1st IAP Product
let prod100coins = response.products[0] as SKProduct
let prodUnlockPro = response.products[1] as SKProduct
print("1st rpoduct: " + prod100coins.localizedDescription)
print("2nd product: " + prodUnlockPro.localizedDescription)
buy100coinsButton.isHidden = false
unlockProButton.isHidden = false
}
}
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
return true
}
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)
} else { print("Purchases are disabled in your device!") }
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
if productIndex == 0 {
print("You've bought 100 coins!")
buy100coinsButton.setTitle("Buy another 100 Coins Chest", for: .normal)
} else {
print("You've unlocked the Pro version!")
unlockProButton.isEnabled = false
unlockProButton.setTitle("PRO version purchased", for: .normal)
}
break
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
print("Payment has failed.")
break
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
print("Purchase has been successfully restored!")
break
default: break
}}}
}
func restorePurchase() {
SKPaymentQueue.default().add(self as SKPaymentTransactionObserver)
SKPaymentQueue.default().restoreCompletedTransactions()
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("The Payment was successfull!")
}
// Buttons -------------------------------------
#IBAction func buy100CoinsButt(_ sender: UIButton) {
productIndex = 0
purchaseMyProduct(validProducts[productIndex])
}
#IBAction func unlockProButt(_ sender: UIButton) {
productIndex = 1
purchaseMyProduct(validProducts[productIndex])
}
#IBAction func restorePurchaseButt(_ sender: UIButton) {
restorePurchase()
}
}
you should create a completion handler so you know when your app actually receives the inApp products, so when the delegate "productsRequest" has completed its operation, for this add to your class the appropriate protocol => "SKRequestDelegate" (all these requests are made asynchronously, I hope you know the differences between synchronous and asynchronous, I recommend you to search on google) Now that you have added the SKRequestDelegate protocol dependency to your class, you can implement the two functions that will inform you of the completion or failure of your request: "requestDidFinish" and "request ... didFailWithError", I'll give you an example with some code below, I hope I've been of help to you, for other clarifications please ask :)
extension ViewController : SKRequestDelegate {
// fine della richiesta in app
func requestDidFinish(_ request: SKRequest) {
debugPrint("[IAP] ---- REQUEST COMPLETED ---- ")
}
// Error Request
func request(_ request: SKRequest, didFailWithError error: Error) {
debugPrint("[IAP] Error: ", error)
}
}
Ah I forgot: check that "self.validProducts.count" is greater than 0 otherwise it means that your array is empty and so when you go to => "purchaseMyProduct(validProducts[productIndex])" your application will crash because the "productIndex" position in your "validProducts" array doesn't exist...
I am trying IAP for the first time and I need some help connecting the dots.
My app has two different non-consumable IAPs which I have set up in a "product" class if its own like this:
enum IAPProduct: String{
case tempoLevels = ".....TempoLevels"
case timingLevels = ".....TimingLevels"
}
Then I set up a helper class via a tutorial I found like this:
class IAPService: NSObject {
private override init() {}
static let shared = IAPService()
var products = [SKProduct]()
let paymentQueue = SKPaymentQueue.default()
func getProducts() {
let products: Set = [IAPProduct.tempoLevels.rawValue, IAPProduct.timingLevels.rawValue]
let request = SKProductsRequest(productIdentifiers: products)
request.delegate = self
request.start()
paymentQueue.add(self)
}
func purchase(product: IAPProduct) {
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) {
self.products = response.products
for product in response.products {
print(product.localizedTitle)
}
}
}
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing: break
default: queue.finishTransaction(transaction)
}
}
}
}
extension SKPaymentTransactionState {
func status() -> String{
switch self{
case .deferred: return "deferred"
case .failed: return "failed"
case .purchased: return "purchased"
case .purchasing: return "purchasing"
case .restored: return "restored"
#unknown default:
return("Something is wrong...")
}
}
}
I would like to trigger some functions in another view controller when the product is purchased. How do I do this?
I sort of set up a function in one of the view controllers that checks UserDefaults in one of the view controllers like this:
func isPurchased() -> Bool {
let purchaseStatus = UserDefaults.standard.bool(forKey: ??)
if purchaseStatus == true {
print("Previously purchased!")
return true
}else{
print("Never purchased!")
return false
}
}
I'm not sure how I can use this function, but if I can somehow, I have code in my app that would work with it if at all possible.
You can use directly SKPaymentTransactionState for purchase checking.
You can call like thi after creating the transaction if want to Bool value
func isPurchased(transaction: SKPaymentTransaction) -> Bool {
return transaction.transactionState == .purchased
}
or directly String value from extension ofSKPaymentTransactionState
func isPurchased(transaction: SKPaymentTransaction) -> String {
return transaction.status
}
In the mean time, you should not ever store a boolean for checking if user has bought in-app purchase in UserDefaults. User can change it very easily (without jailbreaking) and get your goodies for free! You should Use Keychain instead of UserDefaults.
I have no trouble purchasing items one at a time, but when I add two items to the queue in quick succession with SKPaymentQueue.default().add(), the second transaction is never called by updatedTransactions(). Debugging the queue contents shows that the second transaction is in the queue, with a transactionState of .purchasing.
Relevant Code:
// AppDelegate.swift
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
let inAppPurchasesHelper = InAppPurchasesHelper()
inAppPurchasesHelper.setAsTransactionObserver()
}
}
// InAppPurchasesHelper.swift
class InAppPurchasesHelper : NSObject, SKPaymentTransactionObserver {
func setAsTransactionObserver() {
SKPaymentQueue.default().add(self)
}
func buy(productIdentifier: String) {
let product = // product retrieval from app data model using productIdentifier
SKPaymentQueue.default().add(SKPayment(product: product))
print("Added \(item.productIdentifier) to SKPaymentQueue.")
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case SKPaymentTransactionState.purchased:
print("Purchased \(transaction.payment.productIdentifier)")
downloadTransaction(transaction)
break
case SKPaymentTransactionState.failed:
print("Failed \(transaction.payment.productIdentifier)")
failedTransaction(transaction)
break
case SKPaymentTransactionState.restored:
print("Restored \(transaction.payment.productIdentifier)")
downloadTransaction(transaction)
break
case SKPaymentTransactionState.deferred:
print("Deferred \(transaction.payment.productIdentifier)")
break
case SKPaymentTransactionState.purchasing:
print("Purchasing \(transaction.payment.productIdentifier)")
break
}
}
}
func downloadTransaction(_ transaction: SKPaymentTransaction) {
if let downloads = transaction.downloads as [SKDownload]? {
SKPaymentQueue.default().start(downloads)
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) {
for download in downloads {
switch (download.state) {
case .waiting:
break
case .active:
break
case .finished:
finishedDownload(download)
break
case .failed:
failedDownload(download)
break
case .cancelled:
failedDownload(download)
case .paused:
break
}
}
}
}
I am writing my first iOS app in Swift and have been having some problems with the in-app purchases functionality. I think I've got the actual buy function working but I'm not sure about the restore functionality. I'm just providing one product which is a 'pro' version of the app. This is stored in the User Defaults location and having it unlocks the extra functionality. My restore function is confusing me, can someone have a look at it and tell me where I should be setting my User Defaults value? Should it be within the paymentQueueRestoreCompletedTransactionsFinished function or the paymentQueue function?
import UIKit
import StoreKit
class StoreController: UIViewController, SKProductsRequestDelegate,
SKPaymentTransactionObserver {
// Properties
var defaults = UserDefaults.standard
var list = [SKProduct]()
var p = SKProduct()
// Methods
override func viewDidLoad() {
super.viewDidLoad()
// Get list of products for the store
localTitle.isEnabled = false
localDescription.isEnabled = false
price.isEnabled = false
buy.isEnabled = false
restore.isEnabled = false
getProducts()
}
// Outlets
#IBOutlet weak var localTitle: UILabel!
#IBOutlet weak var localDescription: UILabel!
#IBOutlet weak var price: UILabel!
#IBOutlet weak var buy: UIButton!
#IBOutlet weak var restore: UIButton!
// Actions
#IBAction func buy(_ sender: UIButton) {
print("buy pressed")
for product in list {
let prodID = product.productIdentifier
if(prodID == "com.squidgylabs.pro") {
p = product
buyProduct()
}
}
}
#IBAction func restore(_ sender: UIButton) {
print("restore pressed")
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
// Methods
func getProducts() {
print("starting getproducts function")
if(SKPaymentQueue.canMakePayments()) {
print("IAP is enabled, loading")
let productID: NSSet = NSSet(objects: "com.squidgylabs.pro")
let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
print("please enable IAPS")
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("starting productsrequest")
let myProduct = response.products
for product in myProduct {
print("product added")
list.append(product)
}
localTitle.isEnabled = true
localDescription.isEnabled = true
restore.isEnabled = true
buy.isEnabled = true
price.isEnabled = true
// Update labels
localTitle.text = list[0].localizedTitle
localDescription.text = list[0].localizedDescription
// Format the price and display
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .currency
if let formattedPrice = formatter.string(from: list[0].price){
price.text = formattedPrice
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("starting paymentQueueResoreCompletedTransactionsFnished")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "com.squidgylabs.pro":
print("case is correct product ID in payment...finished")
default:
print("IAP not found")
}
}
}
func buyProduct() {
print("buy " + p.productIdentifier)
let pay = SKPayment(product: p)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(pay as SKPayment)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("entering func paymentQueue")
for transaction: AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
switch trans.transactionState {
case .purchased:
print("buy ok, unlock IAP HERE")
print(p.productIdentifier)
let prodID = p.productIdentifier
switch prodID {
case "com.squidgylabs.pro":
print("setting pro in defaults...")
defaults.set(true, forKey: "pro")
default:
print("IAP not found")
}
queue.finishTransaction(trans)
break
case .failed:
print("buy error")
queue.finishTransaction(trans)
break
case .restored:
print("case .restored in paymentQ")
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
queue.finishTransaction(trans)
break
default:
print("Default")
break
}
}
}
}
It may be done like this.
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
completePurchaseForTransaction(transaction)
case .restored:
completePurchaseForTransaction(transaction)
default:
break
}
}
private func completePurchaseForTransaction(_ transaction: SKPaymentTransaction) {
let productIdentifier = transaction.payment.productIdentifier
// Update UserDefaults here
SKPaymentQueue.default().finishTransaction(transaction)
}
You might consider using other class for StoreKit access instead of a UIViewController. Because
Your application should always expect to be notified of completed
transactions.
https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-add
And View controllers come and go. It might even crash the app if you don't remove object you added as an observer to the SKPaymentQueue.
I have simple, well-documented StoreKit library on GitHub that you might check out to get the idea -- https://github.com/suvov/VSStoreKit
So I am trying to put In App Purchase in my app to remove advertisement, but my ad is in another view, and I think I am removing it the wrong way, because it is breaking exactly in the removeallAds function:
func removeallAds() {
ViewController().bannerAd.removeFromSuperview()
}
It is giving me the following error:
Fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
This is my code if you want to take a look:
import UIKit
import StoreKit
class IapViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
#IBOutlet weak var removeAds: UIButton!
#IBOutlet weak var restorePurchase: UIButton!
#IBAction func removeAdsAct(sender: AnyObject) {
for product in list{
var prodId = product.productIdentifier
if (prodId == "com.hazeApps.removeAds"){
p = product
buyProduct()
break;
}
}
}
#IBAction func resPurchaseAct(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
override func viewDidLoad() {
super.viewDidLoad()
removeAds.enabled = false
//IAP Setup
if(SKPaymentQueue.canMakePayments()){
println("IAP is up and running")
var productId: NSSet = NSSet(object: "com.hazeApps.removeAds")
var request: SKProductsRequest = SKProductsRequest(productIdentifiers: productId)
request.delegate = self
request.start()
} else {
println("enable IAPs")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var list = [SKProduct]()
var p = SKProduct()
func buyProduct(){
println("buy " + p.productIdentifier)
var pay = SKPayment(product: p)
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().addPayment(pay as SKPayment)
}
func removeallAds() {
ViewController().bannerAd.removeFromSuperview()
}
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
println("Product Request")
var myProducts = response.products
for product in myProducts{
println("product added")
println(product.productIdentifier)
println(product.localizedTitle)
println(product.localizedDescription)
println(product.price)
list.append(product as SKProduct)
}
removeAds.enabled = true
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
println("Transaction restored")
var purchasedItemIDS = []
for transaction in queue.transactions{
var t: SKPaymentTransaction = transaction as SKPaymentTransaction
let prodId = t.payment.productIdentifier as String
switch prodId{
case "com.hazeApps.removeAds":
println("Remove Adds")
removeallAds()
default:
println("IAP not setup")
}
}
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
println("add payment")
for transaction: AnyObject in transactions{
var trans = transaction as SKPaymentTransaction
println(trans.error)
switch trans.transactionState{
case .Purchased:
println("Unlock IAP here")
println(p.productIdentifier)
let productId = p.productIdentifier as String
switch productId{
case "com.hazeApps.removeAds":
println("Remove Adds")
removeallAds()
default:
println("IAP not setup")
}
queue.finishTransaction(trans)
break;
case .Failed:
println("buy error")
queue.finishTransaction(trans)
break;
default:
println("Default")
break;
}
}
}
func finishTransaction(trans: SKPaymentTransaction){
println("Finish Trans")
}
func paymentQueue(queue: SKPaymentQueue!, removedTransactions transactions: [AnyObject]!) {
println("Removed Trans")
}
}
Other thing, I have some line of code that handle in case of wireless connections to make the bannerAd hide, If the Ad is removed from superview it might get some error in this lines?