iOS's SKPayment addTransactionObserver & defaultqueue, what is the story? - ios

I'm struggling with if I need to add a Transaction observer and also if I need to remove the transaction observer and where and what all this is ...
My inherited code includes the app delegate w/ an observer...
AppDelegate ->
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
SKPaymentQueue *paymentQueue = [self.injector getInstance:[SKPaymentQueue class]];
[paymentQueue addTransactionObserver:self.purchaseHelper];
}
and I'm working on the restore part of the purchase helper...
PurchaseHelper ->
-(void) beginRestorePurchases:(BOOL)serverRestore {
self.serverRestore = serverRestore;
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
-(void) paymentQueueRestoreCompletedTransactionsFinished : (SKPaymentQueue *) queue {
for (SKPaymentTransaction *transaction in queue.transactions) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
My concern at the moment is with Restoring purchases. ( I realize that i'm using a dependency injection pattern for the other portion(the purchasing part) of the code so it different. I'm not sure if this is having an effect on my problem as well.)
The problem is that I have no idea if I should be adding another observer for restoring or not. If I do I get some weird behavior with iTunes prompting multiple times upon restoring. In SO posts I see some mention of removing the observer. When would you do this if I'm creating it on the app delegate level?
Perhaps the my SKPaymentQueue * paymentQueue object is not a defaultQueue object???? No idea....

You always need to add your transaction observer, and early in the app lifecycle. Actions outside your control can cause transactions to fail to complete and the OS level queue will re-feed these to your app. I can't think of any common, legitimate reason to remove the observer.
You only need one observer, and that observer should be the gatekeeper to handling all the IAP events.
You also need to make certain that you're calling finish on each legitimately completed transaction. If you don't, you can wind up stacking lots of duplicated (from the SKU perspective) transactions on top of each other.

Related

Sandbox trying to restore consumable IAP

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];
}

What should the app do in response to a deferred SKPaymentTransaction?

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;
}
}
}

IAP: Receives `paymentQueueRestoreCompletedTransactionsFinished:` callback with unfinished transactions

Some times I get this callback
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
Even before I receive this
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
I don't even get to say finishTransaction:. I find this superweird. I've all this time been assuming paymentQueueRestoreCompletedTransactionsFinished: wouldn't be triggered before I had marked every unfinished transaction as finished.
Is this expected behavior?
This is what I found in documentation
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue
*)queue Description
Tells the observer that the payment queue has finished sending restored transactions. This method is called after
all restorable transactions have been processed by the payment queue.
Your application is not required to do anything in this method.
I'm not sure what it means.
I found the header file itself to be most clear on this part.
#interface SKPaymentQueue : NSObject
...
// Array of unfinished SKPaymentTransactions. Only valid while the queue has observers. Updated asynchronously.
#property(nonatomic, readonly) NSArray *transactions;
#end
#protocol SKPaymentTransactionObserver
// Sent when all transactions from the user's purchase history have successfully been added back to the queue.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
...
#end
These two in conjunction make it unambiguous.
paymentQueueRestoreCompletedTransactionsFinished is triggered once StoreKit has added all transactions to SKPaymentQueue. -[SKPaymentQueue transactions] only has a list of unfinished transactions.
I think the docs could have been more clear about this.

Autorenewable Subscriptions won't be autorenewed

Sorry for that millionth question on autorenewable subscriptions, but i don't get it.
I've done everything as describet in Apples In-App Purchase Guidelines but it didn't solve the problem.
My problem is that i have created autorenewable subscriptions but they won't be autorenewed.
I've create a Payment Transaction Observer class, which implements the SKPaymentTransactionObserver interface. This class will be installed as a paymentObserver at Application startup in the viewDidLoad: method.
PaymentTransactionObserver *observer = [[PaymentTransactionObserver alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
In the paymenttransactionobserver i have the paymentQueue:updateTransactions method: (same as describet in Apple's documentation)
(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
default:
break;
}
}
When i buy a autorenewable product, the product will successfully be purchase.
But it will never be autorenewed. I thought of, that the transaction observer, somehow will get deallocated, but it won't (Otherwhise, i would be notified by the debugger). I also though, i did remove the observer but it will never be removed.
I used the debugger to ensure, that the updateTranscations: method will get called, but nothing. When i buy a test product (in sandbox-mode) with autorenewal time of one week, the method should get called after 3 minutes, but it wont.
What am i doing wrong?
Can anybody help?
Br Nic
If a subscription is autorenewed, the transaction won't pass the paymentQueue:updateTransactions method. The renew just happens on the Store. If you want to test for it you have to either:
Revalidate the receipt on your application server, if you store the receipt there.
Revalidate the receipt on ur iOS client
(http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1)
In order to avoid testing for an autorenew each launch/activation you should store the endDate of the subscription period to test for a renew afterwards.
Also see:
http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/RenewableSubscriptions/RenewableSubscriptions.html#//apple_ref/doc/uid/TP40008267-CH4-SW4
However, there seems to be a bug in the sandbox. Subscriptions sometimes get renewed, sometimes not. Hard to test....
Auto-renewals only get posted to your app's purchase queue on app launch or when it cycles back from being backgrounded. Try clicking the home button and then returning to your app.

Restore already bought in-app-purchases on iPhone?

I got so far: After a reinstall, a user needs to click "buy feature", then he gets scared with the $0.99 question, then has to login and then gets told the feature is already bought and he gets it for free.
I know apple is a religion and users are strong believers, but isn't there a better way? :-) What I want is to check for the feature without actually buying it. Letting the user enter his account info seems to be neccessary, maybe buy a $0.00 feature? or is there a method somewhere that does this?
I'm using MKStoreKit for the whole In-App-Purchase, but any solution would be great.
UPDATE
thanx to darvids0n, your method solved my problem! here's some working code for others trying the same:
- (void)removePreviousPurchases { //just for sandbox testing
[[MKStoreManager sharedManager] removeAllKeychainData];
}
- (void)restorePreviousPurchases { //needs account info to be entered
if([SKPaymentQueue canMakePayments]) {
[[MKStoreManager sharedManager] restorePreviousTransactionsOnComplete:^(void) {
NSLog(#"Restored.");
/* update views, etc. */
}
onError:^(NSError *error) {
NSLog(#"Restore failed: %#", [error localizedDescription]);
/* update views, etc. */
}];
}
else
{
NSLog(#"Parental control enabled");
/* show parental control warning */
}
}
If the $0.99 item is non-consumable, then you should provide a "Restore Purchases" button (or similar) which calls
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
Assuming you've added a transaction observer already, and implemented the protocol including a case to handle a restored transaction (with state SKPaymentTransactionStateRestored) this will work.
Add these two methods :
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue]restoreCompletedTransactions];

Resources