I have the following code for in-app purchases. Since somehow there is a delay as soon as purchase() is executed, I need to indicate that the app is loading. - How can I add an activity indicator as soon as the function purchase() is called?
I've already tried to add a subview with an indicator to the current view controller inside of the given function, but I failed.
class IAPService: NSObject {
...
func purchase() {
guard let productToPurchase = products.first else { return }
print(productToPurchase)
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
func restorePurchases() {
paymentQueue.restoreCompletedTransactions()
}
}
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)
if transaction.payment.productIdentifier == "com.timfuhrmann.savum.premium" && ( transaction.transactionState == .purchased || transaction.transactionState == .restored ) {
premiumPurchased = true
defaults.set(premiumPurchased, forKey: "premiumPurchased")
print(premiumPurchased)
}
switch transaction.transactionState {
case .purchasing: break
case .purchased: goToAddCar(); queue.finishTransaction(transaction)
default: queue.finishTransaction(transaction)
}
}
}
}
As a class, IAPService should not care, or even know, about the user interface. The activity indicator should be initiated and controlled entirely by the view controller.
I would use NotificationCenter observers to let the view controller know when the transaction is complete (or errors out) so the VC knows when to disable the activity indicator and update the rest of the UI.
You can show an activity indicator in the ViewController before that function is called and include a completion handler in the function where you dismiss the activity indicator in the ViewController when the purchase is completed.
You should add the activity indicator inside the ViewController, not from a function that is in you payments module, since the payment module is responsible only for making and processing payments.
Related
I have followed a walkthrough to include a non-consumable in-app purchase. Unfortunately, nothing is happening at all after the user clicks the button for the purchase. I am not getting any prompt to log in to iTunes or to accept the payment. Is there something I am missing here?
I have tried multiple walkthroughs and they all seem to have the similar code, I have followed the steps through the apple website, however I am unsure as to whether I have to do a full app submission before I can test the in-app purchases through a sandbox.
IAPService.swift
import Foundation
import StoreKit
import UIKit
class IAPService: NSObject {
private override init() {}
static let shared = IAPService()
var products = [SKProduct]()
let paymentQueue = SKPaymentQueue.default()
func getProducts() {
let products: Set = [IAPProduct.nonConsumable.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)
}
func restorePurchases() {
print("restore purchases")
paymentQueue.restoreCompletedTransactions()
}
}
extension IAPService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive
response: SKProductsResponse) {
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"
}
}
}
In my products file
IAP.Products.swift
import Foundation
enum IAPProduct: String {
case nonConsumable = "Quizly"
}
In my mainVC
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
IAPService.shared.getProducts()
print("IAP == \(IAPService.shared.products)") // Why is this an empty array?
}
#objc func pressToGetPremium(_ sender : UIButton) {
IAPService.shared.purchase(product: .nonConsumable)
print("IAP ===== \(IAPService.shared.products)")
}
When the user clicks the button I was hoping that a pop up would come up first making the user have to sign in to their apple account and then another pop up would ask if they wanted to accept the non-consumable product ($1.99) etc. But I am not getting a pop up at all.
I am receiving this back from didReceive products......
response SKProductsResponse 0x00000002811cba10
baseNSObject#0 NSObject
isa Class 0x2811cba10 0x00000002811cba10
_internal SKProductsResponseInternal * 0x28139c0e0 0x000000028139c0e0
NSObject NSObject
_invalidIdentifiers __NSSingleObjectArrayI * 1 element 0x00000002811cb930
[0] __NSCFString * "Quizly" 0x000000028139c620
NSMutableString NSMutableString
_products __NSArray0 * 0 elements 0x00000002811c0050
NSArray NSArray
NSObject NSObject
isa Class __NSArray0 0x000001a25a605811
As per our comment discussion - The error produced from the didReceive products method indicates your bundleIdentifiers are not matching the ones on the appStore.
When a button is pressed I use storekit to purchase it. But the issue is the createItems() function and segue are performed before the purchase is confirmed. Is there any way to do this?
func addItems() {
IAPService.shared.purchase(product: .nonConsumable)
createItems()
performSegue(withIdentifier: "returnItems", sender: self)
}
I need the function + segue to be run after the in app purchase has been completed.
IAP function:
func purchase(product: IAPProduct) {
guard let productToPurchase = products.filter({ $0.productIdentifier == product.rawValue}).first else {return}
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
payment Queue function
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)
}
}
}
One simple solution is for the paymentQueue method, when it is called because the purchase has taken place, to post a Notification thru the NotificationCenter. Any other view controller that needs to be informed instantly when the purchase takes place will have registered for this Notification, and thus will hear about it at that moment and can take any desired action.
Keep in mind, too, that you are going to record somewhere, typically in UserDefaults, the fact that the purchase has occurred. Thus any subsequent view controller can always check in its viewDidLoad or viewWillAppear to see whether the purchase has happened, and can modify its behavior accordingly.
I am implementing the non renewable purchase in my app. I am still using in sandbox mode. After I subscribe for the product, when I again try to subscribe the same product, it gives me an alert saying ‘This In-App purchase has already been bought. It will be restored for free.’. I don’t know how I should enable user to subscribe again.
How can I handle multiple user on same device? If one user has paid for the subscription and another user log in into same device to my application he/she should not get the alert as above.
Code :
import StoreKit
class className: SKProductsRequestDelegate
{
var productIDs: Array<String?> = []
var productsArray: Array<SKProduct?> = []
override func viewDidLoad(){
// product IDs for in-app purchase products
productIDs.append(“monthly_subscription_id”) // Monthly
productIDs.append(“yearly_subscription_id”) // Year
requestProductInfo()
SKPaymentQueue.default().add(self)
}
func requestProductInfo() {
if SKPaymentQueue.canMakePayments() {
let productIdentifiers = NSSet(array: productIDs)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers as Set<NSObject> as! Set<String>)
productRequest.delegate = self
productRequest.start()
}
else {
print("Cannot perform In App Purchases.")
}
}
// MARK: SKProductsRequestDelegate method implementation
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if response.products.count != 0 {
for product in response.products {
productsArray.append(product )
}
}
else {
print("There are no products.")
}
if response.invalidProductIdentifiers.count != 0 {
print(response.invalidProductIdentifiers.description)
}
}
// MARK: Buy Subscription button action
#IBAction func btn_purchase_Action(_ sender: Any){
let payment = SKPayment(product: self.productsArray[productIndex]!)
SKPaymentQueue.default().add(payment)
self.transactionInProgress = true
}
}
// MARK: - SKPaymentTransactionObserver
extension className: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction){
print("complete...")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction){
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction){
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
}
I could see popup saying in-app purchase is successful, but "updatedTransaction" function is not called when i successfully finish in-app purchase process.
First time in-app purchase is completed but when i try to purchase the same product again it shows the alert that product is already purchased and could restore for free.
From your code it looks like your transaction observer is a view controller.
If the view controller is dismissed before the payment transaction has been processed then you won't get a chance to complete the transaction.
Your payment queue observer should be an object that is instantiated as soon as your app launches and remains in memory for the lifetime of your app.
Creating the payment queue observer in didFinishLaunching and holding a reference to it in your app delegate is one approach that you can use.
I am building an app with In-App Purchases. The app has 6 products for sale (all consumable).
The app is able to recognize the products (from itunes connect) available for purchase using the following code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Apple Store
appleStoreProducts = [productID, product5ID, product25ID, product100ID, product500ID, product1000ID]
SKPaymentQueue.default().add(self)
self.getProductInfo()
}
func getProductInfo() {
if SKPaymentQueue.canMakePayments() {
for product in appleStoreProducts {
let request = SKProductsRequest(productIdentifiers: NSSet(objects: product) as! Set<String>)
request.delegate = self
request.start()
}
} else {
// Tell user that In-App Purchase is disabled in settings
print("In-App Purchase is disabled in settings")
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
var products = response.products
if (products.count != 0) {
let newProduct = products[0]
switch newProduct.localizedTitle {
case "DOLLAR":
product = newProduct
case "five dollars":
product2 = newProduct
case "twenty five":
product3 = newProduct
case "one hundred dollars":
product4 = newProduct
case "thousand dollars":
product5 = newProduct
default:
print("none of the above")
}
print("titile : \(newProduct.localizedTitle) \n description: \(newProduct.localizedDescription)")
}
else {
print("Product not found")
}
let invalids = response.invalidProductIdentifiers
for product in invalids {
print("Product not found: \(product)")
}
}
Then you can click a button to make the purchase of one of the items, for example:
#IBAction func productOne(_ sender: Any) {
let payment = SKPayment(product: product!)
SKPaymentQueue.default().add(payment)
}
My PROBLEM appears next, when the paymentQueue method is called:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case SKPaymentTransactionState.purchased:
// self.pointsBought()
SKPaymentQueue.default().finishTransaction(transaction)
case SKPaymentTransactionState.failed:
SKPaymentQueue.default().finishTransaction(transaction)
print("transaction failed")
default:
print("ESTDO: \(transaction.transactionState)")
break
}
}
}
This method is called twice when I click once the button. The first time the transactionState is neither "purchased" or "failed", so it calls the default option. The second time the result is "failed". However, I don't know what am I doing wrong.
Relevant information:
I am using an iOS device (iphone 6) to run the app
I am using the developer account (of the app) in my iphone
You are failing to cover every case. You need to cover all of them, and you need to call finishTransaction for certain cases. This is the format of a correct updatedTransactions method:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for t in transactions {
switch t.transactionState {
case .purchasing, .deferred: break // do nothing
case .purchased, .restored:
let p = t.payment
if p.productIdentifier == whatever {
// ... do stuff ...
queue.finishTransaction(t)
}
case .failed:
queue.finishTransaction(t)
}
}
}
So i can't seem to get the updatedTransactions protocol to fire when trying to restore purchases.
I have a button in one view controller which calls the following method in my IAPViewController file restoreIAP() which is set up like so.
func restoreIAP(){
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
This method is called when the user presses the button so this is the class which handles this.
class SettingsViewController: IAPViewController {
#IBAction func restoreDidTouch(sender: AnyObject) {
restoreIAP()
activityTitle = "Restoring"
}
}
In my IAPViewController nothing seems to be triggering this method so that i can do something.
// Check the transaction
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// Check the tranactions
for transaction in transactions {
switch transaction.transactionState {
case .Purchasing:
// TODO: Start Activity Indicator
showPurchaseIndicator(activityTitle)
break
case .Purchased:
// TODO: End the purchasing activity indicator
dismissPurchaseIndicator()
print("Transaction completed successfully.")
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
transactionInProgress = false
// TODO: Put method here to unlock all news sources
storiesMethods.unlockAllStories()
break
case .Restored:
// TODO: Start Activity Indicator
// showPurchaseIndicator(activityTitle)
break
case .Failed:
dismissPurchaseIndicator()
notificationMethods.showAlertErrorMessage(self, title: "Purchase", actionMessage: "Dismiss", message: "Unable to complete transaction please try again later.")
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
transactionInProgress = false
break
default:
print(transaction.transactionState.rawValue)
break
}
}
}
Did your controller added as observer using SKPaymentQueue.defaultQueue().addTransactionObserver(..)?
PS: Have a look at SwiftyStoreKit ( InAppProductPurchaseRequest.swift )
From your description and the code snippets it looks like everything is in the right order.
If the paymentQueue function is never called, your IAPViewController probably doesn't conform to the SKPaymentTransactionObserver protocol, just make it conform:
class IAPViewController: UIViewController, SKPaymentTransactionObserver
and you're good to go.