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];
Related
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.
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;
}
}
}
I check & process IAP receipts (of consumables) on server side, and only call [[SKPaymentQueue defaultQueue] finishTransaction:transaction] on the app when it gets an okay back.
When the server does not return this okay (for whatever reason), the app correctly won't finish the transaction.
My question now is: Is there anything the app needs to do to receive the receipt again for a retry, or does iOS take care of this by invoking - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions`? When I reran my app, the item was re-submitted; but a user should not need to restart the app.
Related question: When I tried to buy a still pending item again (on the sandbox), I got an iOS alert saying that I bought it earlier but was not downloaded. Why is this? I would expect (and have actually seen) this for non-consumables. I could buy another consumable, with this previous one still pending.
Once you finish the transaction of CONSUMABLE IN-APP the following method will fire there add this statement "[[SKPaymentQueue defaultQueue] finishTransaction:transaction]" to remove the purchased product.Using this statement you can avoid the alert message(I bought it earlier but was not downloaded).
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
if (transaction.downloads)
{
[[SKPaymentQueue defaultQueue]
startDownloads:transaction.downloads];
} else {
//Add the following line to remove purchased observer
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
case SKPaymentTransactionStateFailed:
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
break;
}
}
}
The next time your App adds a transaction observer the transaction will appear in the queue.
And perhaps this is why you get the 'not yet downloaded' message - that means 'the app store has not yet gotten a finishTransaction'.
In my previous apps, there is only one IAP, so I didn't realized there was problem in my code. Today I have an app with multiple IAP products, and I found the problem.
I followed Troy's IAP tutorial, and pretty much copied his code. That worked well for my previous apps, as there was only one IAP product in each of those apps. The tutorial uses one IAP product as example, as well. I was trying to post at his blog to ask my question, but my post would be too long and no formatting, plus I think experts here can help me understand more and check if I misunderstood something or missed something.
Now two things I have to handle (unless I missed, the tutorial didn't mention cases of multiple IAP products).
1, I don't know if I misunderstood, in the tutorial, there's a comment for loadStore method, like this
//
// call this method once on startup
//
- (void)loadStore {
// restarts any purchases if they were interrupted last time the app was open
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
// get the product description (defined in early sections)
[self requestProUpgradeProductData];
}
So I call this method at the beginning of my app in the ViewController.m file. My understanding is that this method needs check if there was any previous transaction hanging over there and need take care of it at the moment app re-starts. That's the first statement of this loadStore method. The method continues to call requestProUpgradeProductData, which like this:
- (void)requestProUpgradeProductData {
NSSet *productIdentifiers = [NSSet setWithObject:#"com.runmonster.runmonsterfree.upgradetopro" ];
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
// we will release the request object in the delegate callback
}
Notice here, the first statement in this method is to get IAP product ID. This is all fine for one-IAP-product apps, since the product ID is fixed, and it's okay to call loadStore at the beginning of running the app. Actually, loadStore probably has to run at the beginning of running the app, because, here is the second problem.
2, If loadStore runs at the moment a user wants to buy IAP, rather than at the beginning of the app, the catch is when [productRequest start] runs, the following method will run:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *products = response.products;
proUpgradeProduct = [products count] == 1 ? [[products firstObject] retain] : nil;
if (proUpgradeProduct)
{
NSLog(#"Product title: %#" , proUpgradeProduct.localizedTitle);
NSLog(#"Product description: %#" , proUpgradeProduct.localizedDescription);
NSLog(#"Product price: %#" , proUpgradeProduct.price);
NSLog(#"Product id: %#" , proUpgradeProduct.productIdentifier);
}
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
NSLog(#"Invalid product id: %#" , invalidProductId);
}
// finally release the reqest we alloc/init’ed in requestProUpgradeProductData
[productsRequest release]; //I didn't do this because of ARC
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
}
This will run and return response with products. then I need initiate payment, but I AM NOT SURE HRER HOW SHOULD FOLLOWING METHOD BE CALLED. This is the method initiating IAP payment. As I mentioned, once loadStore runs at the beginning of the app, there are "plenty" time for productRequest....response method (right above) to finish and sending back notification, given that users click here and there for a second. And as I said, for one-IAP-product app, that's fine, I can specify the product ID at the beginning. Now for an app with multiple-IAP-product, I need present multiple products to users, and users choose one of them. Right after this, I need two things in order: A, specify which IAP product, via loadStore -> productRequest method (right above), and B, call the following method to initiate payment.
- (void)purchaseProUpgrade
{
SKPayment *payment = [SKPayment paymentWithProductIdentifier:kInAppPurchaseProUpgradeProductId];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
However, productRequest.....response method may not return yet at the moment I call the payment initiating method. I would not be able to initiate payment. I was thinking to put a delay function in between with a second or so, but I think this approach would be fundamentally wrong. However, how to solve the problem?
Can anyone help here?
The key thing is that you cannot allow the user to purchase anything until you get the response back from the productsRequest. First, initWithProductIdentifiers: takes a set of identifiers. So, on app startup or any other time prior to presenting the purchase option to the user, you call it with the identifiers/skus for ALL products that you may allow the user to purchase. Calling it on startup seems wasteful to me but it should also work and may be the only option for some apps.
Only after you get the productsRequest:didReceiveResponse: callback containing the product objects do you allow the user to click/touch or otherwise invoke the purchase action for that product. You have to check the responses to ensure that the product is present before allowing the user to attempt the purchase. I have a purchase screen and when the user enters it, I invoke the productsRequest with all products relevant to the application's current state (e.g. omitting already purchase items or restricting products by level). I also create the purchase screen but I do not populate it with any products. In the response delegate, I actually populate the screen with the purchasable objects. That way, the user cannot attempt to buy an invalid product.
When the user clicks/taps, you use the product object from the response to create the payment object using :
[SKPayment paymentWithProduct:product]
Note that this is a change from your current setup which uses paymentWithProductIdentifier: - a deprecated method.
This question already has answers here:
Differentiating between initial buy and free "re-buy" in StoreKit/In-App Purchase
(3 answers)
Closed 9 years ago.
Is there a way to find out when a user buys a non-consumable, if he buys it for the first time or has bought it already and gets it again for free?
I checked transactionState, transactionDate of the transaction, but in both cases the data is the same:
transactionState: SKPaymentTransactionStatePurchased (and not SKPaymentTransactionStateRestored in case the user bought it already)
transactionDate: the date at which the transaction was added to the AppStore's payment queue.
You can check Array of transactions which will fill after restoreTransaction method and if Array of transactions is empty it means that user download this upgrade for the first time. In another case you'll check all transactions in Array and compare transaction.payment.productIdentifier with needful product identifire. If it doesn't exist add payment in transaction Array.
For Non-Consumable In-App Purchase I used following code:
#define kInAppPurchaseProUpgradeProductId #"upgradeProductId"
//...
//your payment code for all SKPaymentTransactionStates
//...
//method called when BUY button press
-(void)purchaseProUpgrade{
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
//when restore completed delegate method calls
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
if([[[SKPaymentQueue defaultQueue] transactions] count]==0)
[self addNewPaymentForProductId:kInAppPurchaseProUpgradeProductId];
else
for (SKPaymentTransaction *transaction in [[SKPaymentQueue defaultQueue] transactions]){
if (![transaction.payment.productIdentifier isEqualToString:kInAppPurchaseProUpgradeProductId]){
[self addNewPaymentForProductId:kInAppPurchaseProUpgradeProductId];
break;
}
}
}
-(void)addNewPaymentForProductId:(NSString *)productId{
if([SKPaymentQueue canMakePayments]){
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productId];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
the only disadvantage is that every time you call restoreCompletedTransactions, window will pop up asking you to enter the password of the current user. This solution ensures that the buy window does not appear more than 1 time for each upgrade but all the upgrades will restore every time when you'll try to buy one of them.
Not sure but maybe you can check if transaction.originalTransacion exists or if is different.