In-App Purchase 'Thread 1: Fatal error: Index out of range' - swift

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

Related

I am getting this error [UITextView insertText:] must be used from main thread only - How can I resolve it?

I get an error on the line of code here: self.TextMessage.insertText(countOfItems)
-[UITextView insertText:] must be used from main thread only
I have been struggling to get this to update the text field with the data.
class GameViewController: UIViewController {
#IBOutlet weak var TextMessage: UITextView!
#IBOutlet weak var getUserInput: UITextField!
var userModel = UserModel()
#IBAction func PerformAction(_ sender: Any) {
print("Begin....:" );
if getUserInput.text == "Ready" {
TextMessage.text = "OK Player"
let semaphore = DispatchSemaphore(value:0)
let queue = DispatchQueue(label: "com.run.concurrent", attributes: .concurrent)
queue.asyncAfter(deadline: DispatchTime.now(), execute: {
[weak self] in
guard let self = self else { return }
print("1")
self.userModel.downloadItems()
semaphore.wait(timeout: DispatchTime.now() + 2)
print("2")
semaphore.resume()
let countOfItems = String(self.userModel.users.count)
print("WE PRINT: " + countOfItems)
self.TextMessage.insertText(countOfItems)
})
print("....END" );
}
}
You must do UI updates on main Thread.
DispatchQueue.main.async {
self.TextMessage.insertText(countOfItems)
}

Automatically delete data from Firebase Database

I have seen some other questions asked but I am having trouble getting it to work. I have a Mac app coded in swift and it has a Firebase login but the user types a key in that is stored on Firebase, is there a way to automatically delete that key when the user has successfully used it?
This is my database.
This is the code that is used currently.
import Cocoa
import FirebaseAuth
import FirebaseDatabase
class LoginViewController: NSViewController {
#IBOutlet weak var textUsername: NSTextField!
#IBOutlet weak var textPassword: NSSecureTextFieldCell!
#IBOutlet weak var btnLogin: NSButton!
var keyArray = \[Int64\]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear() {
}
func getLoginState() -> Bool{
let state = UserDefaults.standard.bool(forKey: "isRegistered")
if (state) {
return true
} else {
return false
}
}
override func viewDidAppear() {
let state = self.getLoginState()
if (state){
self.performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "loginsegue"), sender: nil)
self.view.window?.close()
}
var ref: DatabaseReference!
ref = Database.database().reference()
let keyRef = ref.child("key1")
keyRef.observe(DataEventType.childAdded, with: { (snapshot) in
// let postDict = snapshot.value as? \[String : AnyObject\] ?? \[:\]
let keyStr = snapshot.value as? Int64
if let actualPost = keyStr{
self.keyArray.append(actualPost)
}
})
}
#IBAction override func dismissViewController(_ viewController: NSViewController) {
dismiss(self)
}
#IBAction func close(sender: AnyObject) {
self.view.window?.close()
}
#IBAction func onSignup(_ sender: Any) {
// self.performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "gotosignup"), sender: sender)
// self.view.window?.close()
}
func dialogOK(question: String, text: String) -> Void {
let alert: NSAlert = NSAlert()
alert.messageText = question
alert.informativeText = text
alert.alertStyle = NSAlert.Style.warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
#IBAction func onLogin(_ sender: Any) {
//self.btnLogin.isEnabled = false
var isKey = false
if (!self.textUsername.stringValue.isEmpty) {
for key in keyArray{
if(Int64(self.textUsername.stringValue)! == key)
{
UserDefaults.standard.set(true, forKey:"isRegistered")
self.performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "loginsegue"), sender: nil)
self.view.window?.close()
isKey = true
}
}
if (!isKey){
self.dialogOK(question: "Error", text: "Invalid Key")
}
} else {
self.dialogOK(question: "Error", text: "Please Input Key")
}
}
}
You can't sort your database like that and expect a working code, even if there's any. It will make a messy code:
You need to:
Sort your database like [1220:0]. the key first. 0 & 1 as an indicator if it's used or not.
Once the user taps onLogin() you need to set the used key value to 1
Setup Cloud Functions to check if the used key is equal to 1, if yes. then remove the key.
Do the rest of the work.
Related Articles to get you started:
Extend Realtime Database with Cloud Functions
functions.database.RefBuilder

Is my restore purchases code correct and if so where should I put the action to change user defaults?

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

Does not conform to protocol SKPaymentTransactionObserver

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.

Removing Ad with IAP Swift

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?