I have an iOS app with non-consumable IAP Apple hosted content. The downloads are very large (~500MB).
While downloading the content, I have to keep the transaction "open", and only finish the transaction once the download is complete.
The problem with this is, Store Kit will automatically request to re-authenticate every time I move the app from background to foreground (or open the app) while a transaction is on the queue.
So, if I start a download, press the home button, return to app, Store Kit will request me to login.
It doesn't even matter if I re-login or just press "Cancel", the download will continue.
Is there anyway to get rid of this re-authentication request?
You can use SKPaymentTransactionStateDeferred to avoid that. On start up try to restore all transactions except those with SKPaymentTransactionStateDeferred state.
Objective-C:
NSArray *transactionsInQ = [[SKPaymentQueue defaultQueue] transactions];
for(SKPaymentTransaction *transaction in transactionsInQ) {
if(transaction.transactionState != SKPaymentTransactionStateDeferred) {
// your code
}
}
Swift:
let transactionsInQ = SKPaymentQueue.defaultQueue().transactions
for transaction in transactionsInQ {
if (transaction.transactionState != .Deferred) {
// your code
}
}
Related
SKPaymentQueue's restoreCompletedTransactions() does not update the appStoreReceiptURL receipt after completion.
I am working on a Mac app that will be supported via auto-renewing subscriptions. After restoring purchases finishes, I need the updated receipt because I will need to check the current subscription status (expiration date, etc.).
The only solution I could come up with was to refresh the receipt right after restore completed transactions finished, as shown below. This does appear to work. However, the user will be prompted to enter in their username and password twice, which is a less than a perfect experience. In addition, I am concerned that such behavior will lead to a rejection by app review.
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if !fileExistsAtAppStoreReceiptURL() {
print("No receipt.")
}
let refreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
refreshRequest.delegate = self
refreshRequest.start()
}
private func fileExistsAtAppStoreReceiptURL() -> Bool {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else { return false }
return FileManager().fileExists(atPath: appStoreReceiptURL.path)
}
public func requestDidFinish(_ request: SKRequest) {
if request is SKReceiptRefreshRequest {
updateCachedReceiptFromAppStoreReceiptURL()
reverifyKnownPurchases()
notify { self.delegate?.inAppPurchaseController(self, restoreCompletedTransactionsDidFinishWith: .success) }
} else {
notify { self.delegate?.inAppPurchaseController(self, didFinishRequestingIndividualProductInformationWith: .success) }
}
}
Some other notes:
No receipt. does show up in the log.
Because this behavior occurs when using the sandbox environment, I don't know if the behavior is the same when the code executes in production.
The behavior did not change after an attempt with a newly-created test user.
Because I wanted to simulate a real-life situation, I restored purchases on a second Mac, and not the Mac that purchased the subscription.
The rest of the restore process appears to work correctly. paymentQueue:updatedTransactions: is called for each restored transaction with a transactionState of .restored.
SKPaymentQueue.default().finishTransaction(transaction) is called for each restored transaction.
SKPaymentQueue.default().add(self) is called in applicationDidFinishLaunching:.
Although I am making use of an auto-renewing subscription, it does not play a role. A receipt does not appear even if I make use of a non-consumable, for example.
I am not using a server.
The Restoring Purchased Products and the restoreCompletedTransactions() documentation neither explicitly confirm nor deny that the receipt is updated as a part of the restore process. Could this be the expected behavior?
Because I had a similar issue with an iOS test app earlier in the year, this may not be a Mac-only situation.
With all of that in mind, does anyone know how to properly handle such a situation? Has anyone had success with restore + refresh?
this code is what is given on the apple developer website for when a user clicks on the promotional app-store product and it tells to check to see if can complete the transaction? how do I go about checking that? because then I have to cater if the transaction has failed or deferred and can't seem to figure out how to do that.
//MARK: - SKPaymentTransactionObserver
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment,
forProduct product: SKProduct) -> Bool {
// Check to see if you can complete the transaction.
// Return true if you can.
return true
}
There is also the next scenarios I have to cater for which I find to be the same scenario as checking if the transaction can be completed
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment,
forProduct product: SKProduct) -> Bool {
// ... Add code here to check if your app must defer the transaction.
let shouldDeferPayment = ...
// If you must defer until onboarding is completed, then save the payment and return false.
if shouldDeferPayment {
self.savedPayment = payment
return false
}
// ... Add code here to check if your app must cancel the transaction.
let shouldCancelPayment = ...
// If you must cancel the transaction, then return false:
if shouldCancelPayment {
return false
}
}
// (If you canceled the transaction, provide feedback to the user.)
// Continuing a previously deferred payment
SKPaymentQueue.default().add(savedPayment)
)
How do I check to see if the payment failed or needs to be deferred or can be completed as it says in both the code parts?
Whether the transaction needs to be deferred or not depends entirely on your app and any requirements your app may have.
As an example, say your app required the user to set up an account before they can purchase a subscription.
If the user begins the purchase in the App Store, the shouldAddStorePayment method will be called after your app is launched to complete the purchase.
At this point you may detect that the user has not set up an account in your app (indeed, the app could have been installed as a result of them tapping the promoted IAP). In this case you would return false fromshouldAddStorePayment because your app is not in a position to be able to complete the purchase.
Your app would then continue with its normal on-boarding process which gets the user to establish their account.
Once the account is established you want to complete the purchase; this is both a good user experience and ensures that you don't miss a sale.
This is where the other sample code in your question comes in; it shows how you can save the payment and initiate the purchase at a later stage.
In summary, when shouldAddStorePayment is called:
Determine if there is some reason that you cannot complete the purchase now
If there is, return false, otherwise return true
If you return false, save the purchase details so that you can initiate the purchase at a later stage when whatever was preventing the purchase has been resolved.
I have a problem with validating an apple receipt on the server-side.
I tried to find a solution in the internet, but haven't succeeded.
So, description:
First of all, application is made for iOS7. Secondly, I have a few items ( type = Non-Renewing Subscription ). So user can buy one or multiple items and then he should manually renew them ( buy again ).
Applications sends a receipt to the server-side, I make a request to the Apple and get the result with a lot of in_app receipts. Something like:
"in_app":[
{
"quantity":"1", "product_id":"...", "transaction_id":"...",
"original_transaction_id":"...", "purchase_date":"...",
"purchase_date_ms":"...", "purchase_date_pst":"...",
"original_purchase_date":"...",
"original_purchase_date_ms":"...", "original_purchase_date_pst":"...",
"is_trial_period":"..."},
{
"quantity":"1", "product_id":"...",
"transaction_id":"...","original_transaction_id":"...",
"purchase_date":"...", "purchase_date_ms":"...",
"purchase_date_pst":"...", "original_purchase_date":"...",
"original_purchase_date_ms":"...", "original_purchase_date_pst":"...",
"is_trial_period":"..."}
]
So, each "receipt" in "in_app" has transaction_id. But how I can identify the transactionId of the current purchase? I would like to validate it as well and make sure that this is unique.
My concern is: if somebody will get one valid receipt, he will be able to hack our server-side API and make unlimited numbers of in-app purchases with the same valid receipt.
Should I somehow decrypt and check for transaction_id the "original" receipt, the one what I send to Apple for verification?
Any help/suggestions would be highly appreciated.
Thank you in advance.
Regards,
Maksim
#Doug Smith
https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
If you go through the different fields on this page, you will find
Original Transaction Identifier::
For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical to the transaction identifier.
This value corresponds to the original transaction’s transactionIdentifier property.
All receipts in a chain of renewals for an auto-renewable subscription have the same value for this field.
So for your non-auto renewable subscriptions, you have to keep track of two things on your server side:
The original transaction identifier of the receipt that you are validating with itunes server, associate this with the user Id in your database.
Whether the request that you received from the client side is of a Purchase or of a Restore Purchase.
Once you have these two things with you, you can write your logic on these two parameters like below:
::If a request is of type "Purchase" and you already have the original transaction identifier of that receipt associated with some other user Id, you can block that purchase.
::If a request is of type "Restore Purchase" and request is coming from the same user id against which the original transaction identifier is associated in your DB than allow him otherwise block his restore.
Furthermore, you can derive your own logic based on these things, according to your needs.
Let me know if you have any further doubts.
For each new transaction apple send a new receipt which is unique, encode it so no one can forge data.
Get the transaction receipt from the completed transaction encode it and send it to your server, and on the server side decode it and match with the one apple send to server.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
_transactionArray = transactions;
for (SKPaymentTransaction * transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased: {
NSData *receipt = transaction.transactionReceipt;
[self sendReceiptToServer];
} break;
case SKPaymentTransactionStateFailed: {
// finish this transaction
} break;
case SKPaymentTransactionStateRestored:
NSData *receipt = transaction.transactionReceipt;
[self sendReceiptToServer:receipt];
} break;
default:
break;
}
};
}
-(void)sendReceiptToServer:(NSData *)receipt {
// encode receipt
// send receipt to server
// add success and error callback
}
-(void) receiptSuccess {
// finish transaction here
}
-(void) receiptError {
// try again sending receipt to server
}
I have an iOS app that shows the user a set of different news feeds in a PageViewController. Everytime the app starts, it requests the news data from the backend for every single feed. In case it worked fine, a notification via for every single news feed is sent via NSNotificationCenter so the data can be displayed.
In case of an error, a notification for every single feed is sent as well, triggering a popup message that tells the user something went wrong. But if this happens, a popup will be shown for every news feed, up to the amount of added news feeds.
My question is, how can I combine all those error case notifications to a single one and therefore avoid having many useless and annyoing popups?
if (self.isShowingErrorDialog) {
return; // Or possibly cache to show after current one is dismissed.
} else {
[[UIAlertView ...] show];
self.showingErrorDialog = YES;
}
When you send a notification using NSNotificationCentre, you can include user info. This is basically an NSDictionary with additional information.
Why not just include the timestamp of the failed request. You can test this with some fuzziness to see if you've already put up an alert for this batch of requests.
- (void) notificationListener: (NSNotification*) notification {
static NSDate* lastAlerted = nil;
NSDate* sentDate = notification.userInfo[#"RequestDate"];
if ( lastAlerted != nil && [lastAlerted timeIntervalSince:sentDate] > FUZZY_INTERVAL) {
// post alert
// And update last Alerted
lastAlerted = sentDate;
}
}
The method you need is postNotificationWithName:Object:UserInfo:.
Gordon
I don't think you can.
Just to confirm, the notifications you're sending are Apple remote notifications and the alerts are the system alerts popped up by the message centre.
The alerts occur before you get control, as the user has to have the opportunity to ignore them, or else people would use this as a cheat to make apps run in the background and kill user's batteries.
All you can do is send a batch token in your request, and check on the back end.
Good luck
I have an iOS app which has in-app purchase and restore purchases functionality.
When I was testing the app in sandbox mode I use to get the alert all times asking for the password of iTunes account (UserName was already populated).
Now my app is live and I installed it and did the in-app purchase and then restored the purchases, So I am not in sandbox mode still I keep getting the alerts asking for password of iTunes account.
Following is a code which gives the ProductIds that I have purchased earlier,Then I pass those productIds to delegate which changes the status of those from 'Buy' to 'Purchased'
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
//NSLog(#"Restored Transactions are once again in Queue for purchasing %#",[queue transactions]);
NSMutableArray *purchasedItemIDs = [[NSMutableArray alloc] init];
//NSLog(#"received restored transactions: %i", queue.transactions.count);
for (SKPaymentTransaction *transaction in queue.transactions)
{
NSString *productID = transaction.payment.productIdentifier;
[purchasedItemIDs addObject:productID];
// NSLog (#"product id is %#" , productID);
}
if ( mDelegate != nil && [mDelegate respondsToSelector:#selector(purchasedProductList:)] ){
[mDelegate purchasedProductList:purchasedItemIDs];
}
[purchasedItemIDs release];
}
Any idea how to avoid this?
Thanks
You need to ensure that finishTransaction: is called. It must always be called and calling it on a transaction multiple times is fine. If it isn't called, the transaction remains in the queue and the app will try to process it again later. It doesn't matter how the transaction was added to the queue, once it has been acted upon it should be 'finished'.
Note that when restoring new transactions are created, which are effectively wrappers around the original transactions, and they need to be 'finished'. From the restore docs:
The payment queue will deliver a new transaction for each previously completed transaction that can be restored. Each transaction includes a copy of the original transaction.