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.
Related
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.
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 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.
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
}
}
}
I am implementing restore in app purchase. I have a button whose action is
#IBAction func restorePurchases(send : AnyObject){
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
// if (success) {
// I want to do something here
// }
}
My question is.
Is this the right way to restore?
How can we verify success action for restoring purchases?
don't forget to check if you can make a payment:
if (SKPaymentQueue.canMakePayments()) {
SKPaymentQueue.default().restoreCompletedTransactions()
}
for check if the restore was good you have to follow the protocol:
SKPaymentTransactionObserver
and then implement the method:
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!)
and subscribe to the event by doing:
SKPaymentQueue.default().add(self)
Finally here is an exemple of how I check the result:
func paymentQueue(_ queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
print("Received Payment Transaction Response from Apple");
for transaction in transactions {
switch transaction.transactionState {
case .purchased, .restored:
print("Purchased purchase/restored")
SKPaymentQueue.default().finishTransaction(transaction as SKPaymentTransaction)
break
case .failed:
print("Purchased Failed")
SKPaymentQueue.default().finishTransaction(transaction as SKPaymentTransaction)
break
default:
print("default")
break
}
}
}
For some reason, even after trying SKPaymentQueue.default().restoreCompletedTransactions() the following function was never called for me during restoration.
func paymentQueue(queue: SKPaymentQueue!,
updatedTransactions transactions: [AnyObject]!)
This line of code SKPaymentQueue.default().add(self) was added to make my class the observer and still no updates.
It seems that this function gets called with the restored scenario if you try to make a payment all over again and the StoreKit API automatically decides if this should be purchased or restored and shows the message accordingly.
Even thought from a user's point of view, they are not charged again, Apple rejected our app because the restore scenario was not added.
So it seems if you call SKPaymentQueue.default().restoreCompletedTransactions() in your code, handle these additional StoreKit delegates to manage your restoration:
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
handleSuccess()
}
func paymentQueue(_ queue: SKPaymentQueue,
restoreCompletedTransactionsFailedWithError error: Error) {
handleError()
}