Wait until In App Purchase is complete until perform actions - ios

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.

Related

In-App Purchase is successful, but methods are not called

I'm testing a promoted In-App Purchase, as described here.
Here's how it works:
I construct the system URL.
I send the system URL link to my device.
I tap the link.
My app is automatically opened, and the In App Purchase payment sheet is presented.
I tap the "Purchase" button (or whatever it's called), and enter my password to complete the transaction.
The transaction appears to be successful and an alert appears that says, "You're all set. Your purchase was successful".
The problem is, some code that should run depending on the state of the transaction seems not to. I'm not sure how that's possible, but there you go.
So, the deferredTransactionHandler, handlePurchase, handleFailedPurchase, and handleRestoredPurchase methods appear not to be invoked.
class StoreObserver: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate {
var productRequest: SKProductsRequest?
var isAuthorizedForPayments: Bool {
return SKPaymentQueue.canMakePayments()
}
var products = [SKProduct]()
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
//The user has initiated a promoted In-App Purchase directly from the app's product page.
//Show the UI:
SceneDelegate.mainData.showAppStorePromotionUI = true
//"Return true to continue the transaction in your app. Return false to defer or cancel the transaction."
return true
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing: break
case .deferred: deferredTransactionHandler(transaction)
case .purchased: handlePurchase(transaction)
case .failed: handleFailedPurchase(transaction)
case .restored: handleRestoredPurchase(transaction)
#unknown default: fatalError("unknownPaymentTransaction")
}
}
}
}
How do I know these methods aren't being called? 1) Inside these methods, I update the UI to reflect that the transaction has completed (the UI is never updated, though), and 2) Inside these methods I try to present an alert (but the alert is never presented). Here's how I'm doing that:
func handlePurchase(_ transaction: SKPaymentTransaction) {
//Update the UI
SceneDelegate.mainData.showAppStorePromotionUI = false
//Present an alert so you know this function ran
let alert = UIAlertController(title: "Alert", message: "handlePurchase()", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
}))
let topViewController = UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.rootViewController
topViewController?.present(alert, animated: true, completion: nil)
//Complete the transaction
SKPaymentQueue.default().finishTransaction(transaction)
}
Why does it seem that these functions are not being called? What am I missing?
Thank you!
Sigh. It was a stupid mistake on my part.
I'd been testing something, and had forgotten to remove a line in my SceneDelegate's sceneWillResignActive(_:) in which I removed the observer. So, every time the payment sheet was presented, the observer was removed, which messed everything up.
func sceneWillResignActive(_ scene: UIScene) {
//Removing this line solved the problem
//SKPaymentQueue.default().remove(SceneDelegate.inAppPurchases)
}

Add activity indicator inside of non view controller class

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.

Update UITableView after in-app purchase

I have the following code in my SKPaymentTransactionObserver and I've confirmed that it runs the .purchased case when I complete a sandbox purchase.
func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing, .deferred: break
case .purchased, .restored:
Settings.didPurchase = true
DispatchQueue.main.async {
tableView.reloadData()
}
SKPaymentQueue.default().finishTransaction(transaction)
case .failed: // other code here
}
}
}
The data source for my UITableView includes this function:
func numberOfSections(in tableView: UITableView) -> Int {
return Settings.didPurchase ? 3 : 4
}
While debugging, I confirmed that this function is called after I call tableView.reloadData() and that it returns 3 after the purchase. But, the view doesn't update to include the extra section. It only updates if I close the entire view and reload it after completing the purchase, in which case it triggers another table view reload and looks like it's supposed to.
My theory is that because the UITableView isn't the top-level view when I call reloadData() (it's covered by an alert from Apple saying that the purchase was successful), it's not updating the view, thinking it's not visible. But, is this reasonable? And either way, how can I fix my code so that the view updates as expected when the user completes a purchase?

Restore Purchase : Non-Consumable

I have completed a small app where I have a non-consumable purchase option. It is on the App Store.
The purchase of the product runs OK. It's my Restore Purchase function that seems to do nothing.
I have added this code for the Restore Purchase #IBAction:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
But nothing happens when I hit the restore purchase button.
I think I have to add a function that checks if the restore was successful or not. Am planning to amend code to the following:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Restored:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Sorry", message: "Your purchase(s) could not be restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
default:
break;
}
}
}
Will this do the trick?
I have been through every thread in relation to effecting Restore Purchase transactions, and my research has led me to the above. So I don't think this is a duplicate of a question, but perhaps may clarify how to successfully restore purchases for others facing my similar situation.
Your codes looks pretty fine for the most part, although some parts seem to be from older tutorials . There is some changes you should make, one of them is that you need to call your unlockProduct function again.
This is the code I use (Swift 3).
/// Updated transactions
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
// Transaction is being added to the server queue.
case .purchased:
// Transaction is in queue, user has been charged. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
let productIdentifier = transaction.payment.productIdentifier
unlockProduct(withIdentifier: productIdentifier)
case .failed:
// Transaction was cancelled or failed before being added to the server queue.
defer {
queue.finishTransaction(transaction)
}
let errorCode = (transaction.error as? SKError)?.code
if errorCode == .paymentCancelled {
print("Transaction failed - user cancelled payment")
} else if errorCode == .paymentNotAllowed { // Will show alert automatically
print("Transaction failed - payments are not allowed")
} else {
print("Transaction failed - other error")
// Show alert with localised error description
}
case .restored:
// Transaction was restored from user's purchase history. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
if let productIdentifier = transaction.original?.payment.productIdentifier {
unlockProduct(withIdentifier: productIdentifier)
}
case .deferred:
// The transaction is in the queue, but its final status is pending external action
// e.g family member approval (FamilySharing).
// DO NOT freeze up app. Treate as if transaction has not started yet.
}
}
}
Than use the delegate methods to show the restore alert
/// Restore finished
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
guard queue.transactions.count != 0 else {
// showAlert that nothing restored
return
}
// show restore successful alert
}
/// Restore failed
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
/// handle the restore error if you need to.
}
Unlock product is just a method I am sure you already have too.
func unlockProduct(withIdentifier productIdentifier: String) {
switch productIdentifier {
/// unlock product for correct ID
}
}
As a side note, you should move this line
SKPaymentQueue.default().add(self)
out of your restore and buy function and put it in viewDidLoad.
Apple recommends you add the transaction observer as soon as your app launches and only remove it when your app is closed. A lot of tutorials unfortunately dont teach you this correctly. This way you unsure that any incomplete transactions e.g due to network error, will always resume correctly.
https://developer.apple.com/library/content/technotes/tn2387/_index.html
In my real projects my code for IAPs is in a Singleton class so I would actually using delegation to forward the unlockProduct method to my class that handles gameData. I can than also make sure the observer is added at app launch.
Hope this helps
Took me a while to suss out, but the reason my StoreKit was not updatingTransactions and restoring the purchase was because of a broken Configuration setting in my app's Scheme. When I set that to None, it worked!
In Xcode I went into Edit>Scheme (image1) clicked on the Run>Options tab and selected None for StoreKit Configuration (image2). I also went on my physical device, and logged out of my personal Apple purchase account (Settings >Your Name/Pic at the Top > Media & Purchases > Sign Out) (image3). And finally, this step might not be critical, but I logged into my test sandbox account on my device at the bottom of the Settings>App Store menu(image4 and image5). That is an account that I setup in developer.apple.com under test users.

Restoring purchases in swift

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.

Resources