I use code for in-app purchase based on this answer. But I ran into some strange bugs. For example:
I have FirstViewController with locked content and PurchasesViewController with purchase buttons. When I click on purchase button in PurchasesViewController, confirm purchase and wait several seconds for notification about the purchase is successful. Next I go back to FirstViewController and see that content unlocked. In this example all works fine. But...
Problem:
If I click on purchase button in PurchasesViewController, confirm purchase and without waiting for the notification, I go back to FirstViewController and receive a notification there. My content not unlocked. Even if I restart the app content not unlocked. But if I click on restore purchases button all start works fine and locked content will be unlocked.
This problem can be confusing to the user. So I want to lock the interface and show the activity indicator until the user receive a notification about the purchase is successful. And after user click "Ok" in notification windows I want to unlock interface and remove activity indicator. But how to do it? Where and when I should call lock interface function?
On my projects I usually use https://github.com/SVProgressHUD/SVProgressHUD for show loadings. And on your case you need to show SVProgress.show() when user tap on purchase button. And add SVProgress.dismiss() when all purchased finished SVProgress.showErrorWithStatus(error.localizedDescription) when you have payment issue.
On SKPaymentTransactionObserver have func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) on this function you can check status paymentQueue like this example:
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 {
queue.finishTransaction(t)
SVProgress.dismiss()
}
case .failed:
queue.finishTransaction(t)
SVProgress.showErrorWithStatus("Error message")
}
}
}
Related
How can I distiguish the condition when a user is hitting restore but has never bought that IAP package before.
When I trigger
SKPaymentQueue.default().restoreCompletedTransactions()
the system triggers the following callbacks
1) For an already-bought item those 2 in that order
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
2) For an not-yet-bought item simply
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
With this possible combinations, how can I ensure / verify that the call 'restore' was called, but the result is "not yet bought".
Do I really have to save the state "paymentQueue was not yet called and now paymentQueueRestoreCompletedTransactionsFinished was called" as condition?
All you need to do is process any purchases that are delivered to updatedTransactions as you do for the initial purchase case. Ie. unlock content or provide additional functions and persist the purchase in keychain or whatever method you are using.
paymentQueueRestoreCompletedTransactionsFinished lets you know that the restore operation is complete. You can use this method to update your UI if required. For example if you showed an activity indicator when you started restoring then you can use this callback to remove that indicator.
I've been trying to test some consumable IAP on iOS, and I'm getting a strange error. An alert pops up with the text:
"This In-App Purchase has already been bought. It will be restored for
free. [Environment: Sandbox]"
I have checked, and I'm certain that my IAP is consumable in iTunesConnect. It seems that my validation process is somehow failing, but I wouldn't expect this error message. Does anyone have any experience with this?
I'm not sure if this is the correct action, but calling:
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
on the repeating transaction cleared them out. I suspect that I never called it in the success case due to stopping in the debugger or something.
Probably you solved your issue already, but i met the same issue and have some additional solution points:
Sorry, i use Swift, but I think it's understandable here.
First of all, there are two important moments in your code, which you must cover:
class InAppPurchaseViewController: UIViewController , SKProductsRequestDelegate
, SKPaymentTransactionObserver //NOTE THIS!
{
override func viewDidLoad() {
super.viewDidLoad()
//
...
...
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
// here you create the observer
}
//before you leave this ViewController you should delete this Observer.
//I had a bug which worked like this: when user leaved the IAPViewController without
//buying something and then came back, viewDidLoad added one more Observer
// so App crashed on the attempt to buy
// for example:
#IBAction func exitTapped(sender: AnyObject) {
dismissViewControllerAnimated(true) { () -> Void in
SKPaymentQueue.defaultQueue().removeTransactionObserver(self)
}
}
after you added the observer, don't forget to finish the transaction:
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .Restored:
print("Restored")
break
case .Failed:
print("Failed")
//FINISH TRANSACTION
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
break
case .Purchasing:
print("Purchasing")
break
case .Purchased:
// DO your stuff here
// ...
// and FINISH TRANSACTION
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
}
break
case .Deferred:
print("Deferred")
break
}
}
}
One more issue, that I solved here, is: I forgot to add the finish transaction line of code, so i had open transaction with consumable IAP.
When i tried to buy one more time with this Sandbox User, i received the message "This In-App Purchase has already been bought. It will be restored for free. [Environment: Sandbox]" This happened again and again even after I added FinishTransaction. I changed the sandbox user. App became to prompt FirstSandBoxUser iTunes Password every time i ran the App. Catastrophe... Very annoying...
Every time I tried to make this purchase with FirstSandBoxUser, the transaction.transactionState was .Purchasing, and you can't finish the transaction in this state!
I made an assumption, that if transaction is not finished, StoreKit takes it like a non-consumable, so I should start a restoration process, and then catch a moment to finish the transaction. This is the code:
func paymentQueue // see above
//....
case .Purchasing:
print("Purchasing")
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
break
//...
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
for transaction in queue.transactions {
queue.finishTransaction(transaction)
}
}
It's enough to run this code once, I think, it will catch all "Purchasing" transaction, Restore them and after that Finish.
Of course, after fixing the issue remove this additional code.
Good luck!
This happens when the transaction was not finished on the client using.
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
You should register a transactionObserver in your apps startup phase, to ensure you will get notified of any unfinished transactions. You should finish those transactions and do whatever else is needed to correctly deliver the purchased product to the user.
From apples programming guide (emphasis mine):
Register a transaction queue observer when your app is launched, as shown in Listing 5-1. Make sure that the observer is ready to handle a transaction at any time, not just after you add a transaction to the queue. For example, consider the case of a user buying something in your app right before going into a tunnel. Your app isn’t able to deliver the purchased content because there’s no network connection. The next time your app is launched, StoreKit calls your transaction queue observer again and delivers the purchased content at that time. Similarly, if your app fails to mark a transaction as finished, StoreKit calls the observer every time your app is launched until the transaction is properly finished.
They recommend the following code to register the observer:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
/* ... */
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
}
I'm building an app with Swift and I just added StoreKit support.
There is only consumable products in my app.
AppDelegate follows the SKPaymentTransactionObserver protocol and here's my paymentQueue:updatedTransactions method:
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction in transactions as [SKPaymentTransaction] {
switch transaction.transactionState {
case SKPaymentTransactionState.Purchasing:
println("purchasing")
case SKPaymentTransactionState.Purchased:
println("payment done")
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
case SKPaymentTransactionState.Failed:
println("payment failed")
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
default:
println("nop \(transaction.transactionState)")
}
}
}
I've created a test user in iTunes connect and so on and everything works fine except all transactions failed whatever I do.
Transactions go to purchasing state and then failed, even if I confirm the transaction in simulator.
Any idea why this happening?
Thanks.
The official word from Apple is that StoreKit doesn't work in the simulator.
These days, with an iOS 8 target in the current simulator quite a lot of it does work, like fetching the list of products, or putting up the purchase confirmation dialog. However the final purchase still fails in the simulator, and that appears to be deliberate on Apple's part.
You Can Test IAP in Your ios6.1 Simulator and yes iOS 8.0 Simulator Also Gives You Response Alert But DoesNot Provide Purchase Functionality In Simulator
As #w0mbat says the hole flow is working now except for the actual confirm transaction, in order to manage this you can do the following
Import target conditionals header
#include "TargetConditionals.h"
Modify your failed transaction method
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
if (TARGET_IPHONE_SIMULATOR && DEBUG && (transaction.error.code==0))
{
[self completeTransaction:transaction];
return;
}
...
}
I have in-app purchases in my app, and new to iOS 8 are "deferred" transactions, partially described in a tech note
I understand what it does and that I need to not block the UI, and update my UI to reflect that the transaction state is deferred. But what am I supposed to place in the method -(void)transactionDeferred:(SKPaymentTransaction *)transaction to disregard the transaction for the time being?
Do I only have update the UI? Also what should the content of the UI be? Do I need to replace the price label with something like "Your purchase is deferred"? I don't think there is a way to test this, at least I haven't seen anything about it with my sandbox test account. If there was a way to go through the process and see how it works, it would make a lot more sense to me.
What I am doing is:
Stopping indicator animation
Activating buy and restore buttons
Showing an alert:
Waiting For Approval
Thank you! You can continue to use Altershot while your purchase is pending an approval from your parent.
I watched WWDC 14 video. Apple says you should't block UI and allow to click on buy button again. I think we need this in case parent miss alert, so child can send one more.
What I know is that we should not call following method for deferred transactions:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
The code bellow will allow you to check if the product ID you want to sell is in deferred mode. Use it to update the UI accordingly.
if ([[SKPaymentQueue defaultQueue].transactions count] > 0) {
for (SKPaymentTransaction *transaction in [SKPaymentQueue defaultQueue].transactions) {
if ([#"your.product.id" isEqualToString:transaction.payment.productIdentifier]) {
if (transaction.transactionState == SKPaymentTransactionStateDeferred) {
// update UI that you are still waiting for parent approval. You'll get "PURCHASED" if parent approved or "FAILD" if parent declined or 24 hours passed since request.
}
break;
}
}
}
As discussed in this question and everywhere else, Apple now requires apps to include a means for the user to restore completed transactions for In App Purchases.
I'm all for this. The first version of my app somehow made it past review without it (I wasn't aware of this rule at the time, and/or it wasn't being enforced yet), but then I started receiving lots of e-mails from users asking about missing content (there is the Data Storage Guidelines too, and the heavy, downloadable contents aren't backed up).
So let's say I include a 'restore' button somewhere in my UI, that when tapped calls:
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
So far, so good. The user is prompted his AppleID and/or password, and the restoring process begins.
The problem I have is: If there is no transactions to restore, after the AppleID prompt essentially nothing happens in my app, and that may be confusing to the user or make the app look unresponsive or broken.
I would like to be able to display an alert view along the lines of "All purchases are up to date" or something.
Is there anything I can do in my Transaction Observer code to detect this case?
Does anybody think it would be a bad design, UX-wise?
This is still an issue in the latest SDK / xCode 8.0, Swift 3 - if a user who hasn't made any purchases attempts to 'restore', the following method:
SKPaymentQueue.default().restoreCompletedTransactions()
does not trigger the usual delegate method that handles purchases / restoring:
paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {...}
Interestingly, the method that probably should catch the error is also NOT called:
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error){...}
And instead, the optional method is triggered, as if the restore worked fine:
func paymentQueueRestoreCompletedTransactionsFinished()
This can cause the app to look like it is hanging / not doing anything.
As discussed in the other answers, the cause of this is that the SKPaymentQueue doesn't contain any transactions.
in Swift, this problem can be overcome by using the following:
//Optional Method.
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue)
{
let transactionCount = queue.transactions.count
if transactionCount == 0
{
print("No previous transactions found")
//You can add some method to update your UI, indicating this is the problem e.g. use notification centre:
NotificationCenter.default.post(name: "restoreFailedNoPrevIAP", object: nil)
}
}
Importantly, if a user has made previous purchases, the transaction queue will not be empty, hence updateTransaction delegate method will be called, and will process the restore request normally.
You could also implement the following delegate functions:
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
-(void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
Then you would know when the restore process was finished or if it failed.
You can mix in the usage of queue.transactions.count in paymentQueueRestoreCompletedTransactionsFinished to see if any transactions was restored.
Remember to handle SKPaymentTransactionStateRestored in
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
You might also want to handle the restored transaction(s) the way same as you did with SKPaymentTransactionStatePurchased the transaction(s).
I was interested in the right/best wording for restoring purchase.
I have seen enough "Unknown Error" alerts, just using [error localizedDescription] inside -(void)paymentQueue:restoreCompletedTransactionsFailedWithError:. (todo: fill radar)
So I took a look at how Apple does it. The only app from Apple with Non-Consumable In-App Purchases right now is GarageBand (Dec, 2014).
Instead of "Restore Purchase", "Restore Previous Purchases" or ... they go with "Already Purchased?".
But here is the screen I'm more interested in, the result of pressing "Already Purchased?" when there is nothing to restore:
"There are no items available to restore at this time." Not revolutionary, but beats the hell out of "Unknown Error"
So lets look at -(void)paymentQueue:restoreCompletedTransactionsFailedWithError:.
iOS:
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
if ([error.domain isEqual:SKErrorDomain] && error.code == SKErrorPaymentCancelled)
{
return;
}
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(#"There are no items available to restore at this time.", #"")
message:nil
delegate:nil
cancelButtonTitle:NSLocalizedString(#"OK", #"")
otherButtonTitles:nil];
[alert show];
}
OS X:
I'm not happy with just the same text on OS X. An NSAlert with just the messageText and no informativeText just looks empty and wrong.
One option for me is to let the user know that he needs to purchase it, with something like "To use it, you need to buy “%#”.".
Another option I came up with is letting them browser there Purchase History. I found that you can directly link to it with itms://phobos.apple.com/purchaseHistory. In all honesty Purchase History in the iTunes Store is a piece of shit, it gonna take you for ever do find something.
But maybe it helps reinsuring people that we don't try to make them repurchase something. Always assume that your customers don't know or can't tell the difference between Non-Consumable and Consumable. And don't know that they can't get charged twice for a Non-Consumable.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
if ([error.domain isEqual:SKErrorDomain] && error.code == SKErrorPaymentCancelled)
{
return;
}
NSAlert *alert = nil;
alert = [NSAlert alertWithMessageText:NSLocalizedString(#"There are no items available to restore at this time.", #"")
defaultButton:NSLocalizedString(#"OK", #"")
alternateButton:NSLocalizedString(#"Purchase History", #"")
otherButton:nil
informativeTextWithFormat:#"You can see your purchase history in the iTunes Store."];
NSModalResponse returnCode = [alert runModal];
if (returnCode == NSAlertAlternateReturn)
{
NSURL *purchaseHistory = [NSURL URLWithString:#"itms://phobos.apple.com/purchaseHistory"];
[[NSWorkspace sharedWorkspace] openURL:purchaseHistory];
}
}
Example on OS X
Testing Notes (OS X, itunesconnect sandbox user):
When user clicks cancel:
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
Error Domain=SKErrorDomain Code=2 "The payment was canceled by the user" UserInfo=0x600000470a40 {NSLocalizedDescription=The payment was canceled by the user}
When there is nothing to restore:
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
Error Domain=SKErrorDomain Code=0 "Unknown Error." UserInfo=0x60800007fb80 {NSLocalizedDescription=Unknown Error.}
I ran into the same issue in the app I'm working on now. My workaround is to use a X second timer. It starts when you tap the 'Restore Purchases' button and restarts if a restored transaction event comes in. Once it reaches the X second mark I have a popup saying "Purchases Restored". So if you have no transactions you should only have to wait X seconds. Hope that helps.