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)
}
Related
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.
Why is paymentQueue(:shouldAddStorePayment::) not being called?
I am sure I've done everything I need to do.
I declared my own class that supports the SKPaymentTransactionObserver protocol:
import UIKit
import StoreKit
import AudioToolbox.AudioServices
class UTIPaymentTransactionObserver: NSObject, SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
print("!!! shouldAddStorePayment")
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
return false
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("!!! updatedTransactions")
for transaction in transactions {
print("!!! transaction=", transaction)
switch transaction.transactionState {
// Call the appropriate custom method for the transaction state.
case SKPaymentTransactionState.purchasing:
showTransactionAsInProgress(transaction, deferred: false)
case SKPaymentTransactionState.deferred:
showTransactionAsInProgress(transaction, deferred: true)
case SKPaymentTransactionState.failed:
failedTransaction(transaction)
case SKPaymentTransactionState.purchased:
completeTransaction(transaction)
case SKPaymentTransactionState.restored:
restoreTransaction(transaction)
}
}
}
func showTransactionAsInProgress(_ transaction: SKPaymentTransaction, deferred: Bool) {
print("!!! showTransactionAsInProgress")
}
func failedTransaction(_ transaction: SKPaymentTransaction) {
print("!!! failedTransaction")
SKPaymentQueue.default().finishTransaction(transaction)
}
func completeTransaction(_ transaction: SKPaymentTransaction) {
print("!!! completeTransaction")
SKPaymentQueue.default().finishTransaction(transaction)
}
func restoreTransaction(_ transaction: SKPaymentTransaction) {
print("!!! restoreTransaction")
}
}
I added code to vibrate the device when paymentQueue(:shouldAddStorePayment::) is called to indicate that the method is actually called.
I declared an instance of the observer class globally:
internal let paymentTransactionObserver = UTIPaymentTransactionObserver()
I made sure I added the observer in AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
SKPaymentQueue.default().add(paymentTransactionObserver)
return true
}
The print statement in the paymentQueue(:shouldAddStorePayment::) method never prints and the device never vibrates. It doesn't look like that method is called.
The paymentQueue(_:updatedTransactions:) method is called. The print statement in that method executed.
In this code I returned false for the paymentQueue(:shouldAddStorePayment::) method, but it doesn't make a difference. The process goes through just as if I returned a true. The product had already been purchased before, so it goes through and lets the user/tester purchase it again.
Any help would be appreciated.
Here is code extension for the view controller that retrieves the product from App Store and presents my user interface that allows the user to purchase the product:
I call validateProductIdentifiers() to start the process of selling the product to the user.
// MARK: - SKProductsRequestDelegate
extension CloudViewController: SKProductsRequestDelegate {
func validateProductIdentifiers() {
let url = Bundle.main.url(forResource: "Purchase", withExtension: "plist")!
let nsArrayProductIdentifiers: NSArray = NSArray(contentsOf: url)!
let productIdentifiers = nsArrayProductIdentifiers as! [String]
print(productIdentifiers)
let setProductIdentifers: Set = Set(productIdentifiers)
let productsRequest = SKProductsRequest(productIdentifiers: setProductIdentifers)
self.productsRequest = productsRequest
productsRequest.delegate = self
productsRequest.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("!!! didReceive")
self.products = response.products
let alertMessage = "Would you like to purchase?"
let alert = UIAlertController(title: nil, message: alertMessage, preferredStyle: .actionSheet)
let actionYes = UIAlertAction(title: "Yes", style: .default) {
action in
// Purchase
let product: SKProduct = response.products.first!
let payment: SKMutablePayment = SKMutablePayment(product: product)
SKPaymentQueue.default().add(payment)
}
let actionNo = UIAlertAction(title: "No", style: .cancel, handler: nil)
alert.addAction(actionYes)
alert.addAction(actionNo)
alert.popoverPresentationController?.barButtonItem = barButtonItemEnableDropbox
present(alert, animated: true, completion: nil)
}
}
As per the documentation for this method
This delegate method is called when the user starts an in-app purchase in the App Store, and the transaction continues in your app. Specifically, if your app is already installed, the method is called.
This occurs when a user redeems a promo code for an in-app purchase in the App Store app or purchases a promoted in-App purchase in the App Store app.
It is not called when the purchase is initiated in your app, since you already have control over whether purchasing should be permitted when the user is in your app.
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.
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.
My sandbox test account can purchase non-consumable item and restore it. Everything works. However, if the account have not purchased the item before, pressing the restore button does nothing. I see nothing in the debug panel. I'm expecting iOS to detect if a certain user has purchased the item or not, if not then display a message asking them to buy it. Does it work like that or the current behavior is totally acceptable?
Here is the restore purchase code (Swift) connecting to a button inside the main storyboard:
#IBAction func restoreButtonPressed(sender: UIButton) {
statusLabel.text = "Status: Restoring Purchase"
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
Other implemented methods include:
Works for normal purchase
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {}
Works for normal restore
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {}
Never see messages coming from this method before
func paymentQueue(queue: SKPaymentQueue!, restoreCompletedTransactionsFailedWithError error: NSError!) {}
Thanks!
You can check if the queue has any returned transactions, and if not it means that there are no purchases to restore:
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
if queue.transactions.count == 0 {
let alert = UIAlertView()
alert.title = "Oops"
alert.message = "There are no purchases to restore, please buy one"
alert.addButtonWithTitle("Buy")
alert.addButtonWithTitle("Cancel")
alert.show()
}
}
you can try this :
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing: print("Payment is being processed")
case .purchased: print("USER PURCHASED YOUR PRODUCT")
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
if transaction.original?.transactionIdentifier != nil { // this identifier uniquely identifies a completed tansaction.
// queue.restoreCompletedTransactions()
print("THE BUYER BOUGHT THIS PRODUCT BEFORE")
} else {
/*
display UIAlertController showing an error
there is no receipts, user never purchased this product.
*/
}
case .failed:
self.dismiss(animated: true, completion: nil)
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("pending")
#unknown default:
break
}
}
}