In App Purchase - Works in Development, not in Production - ios

I have an iOS app that has in app purchase, and when testing it in production, I see the "your purchase was successful" message, but do not see the product delivered. The code to update the balance of "currency" within my app is not being called.
This however, has worked in development.
I have
-the product is cleared for sale
-all banking contracts are valid and live
Adding some code here (this is the finished transaction method that's called from the "updatedTransaction" delegate callback method:
-(void)finishedTransaction:(SKPaymentTransaction *)transaction
{
NSLog(#"PURCHASE MANAGER - finishedTransaction:");
// call finish transaction on the queue.
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
// Old Deprecated
// [self checkReceipt:transaction.transactionReceipt];
[self checkReceipt:receiptData];
// set purchasing flag
purchasing = FALSE;
}
The checkReceipt method creates JSON payload and sends it to the verifyReceipt URL.
My question is: if transaction.transactionReceipt is deprecated, can I still use it? also, if the appStoreReceiptURL is from the bundle, how does it have the transaction data?

Related

StoreKit: Receipt validation problems

I am struggling with this for quite some time. First off our problem: In our app we have renewable subscriptions and a one time purchase. I want to read from the receipt if a subscription is still valid or if it is a one time purchase (which is valid lifetime). First off one question:
What does this file contain?
NSData* receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
I only have to check if this file is present and if not I request a refresh correct? But if a subscription has been auto renewed do I need to refresh this file as well? Or does the receipt get updated when I verify it with the apple server?
Ok now my process is as follows and starts with the payment queue:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
if(SANDBOX_TESTING) NSLog(#"updated transaction");
[self refreshReceipt];
self.transactionCount = [transactions count];
if(SANDBOX_TESTING) NSLog(#"Number of transactions: %ld", (long)self.transactionCount);
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction restore:NO];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
[[NSNotificationCenter defaultCenter] postNotificationName:IAPProductPurchaseStateChangedNotification object:nil];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
NSLog(#"Restore transaction started received");
//Only restore transactions that haven't been restored yet AND if they have a still supported identifier
//IMPORTANT: Original Transaction is only set (transaction.originalTransaction.transactionIdentifier) if it is a restore!
//This first if only helps in a restore case, that not all subscription renewals get looped. If a valid subscription is found for one subscription type, the loop runs only once
if(![self.restoredTransactions containsObject:transaction.payment.productIdentifier] && [[self getAllPurchaseIDsForPlatformType:PURCHASESONPLATFORM_IOS] containsObject:transaction.payment.productIdentifier]){
[self completeTransaction:transaction restore:YES];
} else {
self.transactionCount--;
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
//Moved from paymentQueueRestoreCompletedTransactionsFinished
if(self.transactionCount == 0){
[self.delegate restoredTransactions:[self.restoredTransactions count] withReceipt:self.appleReceipt];
}
break;
default:
break;
}
};
}
So in refreshing the receipt I just do this:
-(void)refreshReceipt{
//TODO: Check if this file exists - if not refresh receipt (and only then...)
NSData* receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString *payload = [NSString stringWithFormat:#"{\"receipt-data\" : \"%#\", \"password\" : \"%s\"}",
[receiptData base64EncodedStringWithOptions:0], "xxx"];
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
//Sending the data to store URL based on the kind of build.
NSURL *storeURL;
if(SANDBOX_TESTING){
storeURL = [[NSURL alloc] initWithString:#"https://sandbox.itunes.apple.com/verifyReceipt"];
} else {
storeURL = [[NSURL alloc] initWithString:#"https://buy.itunes.apple.com/verifyReceipt"];
}
//Sending the POST request.
NSMutableURLRequest *storeRequest = [[NSMutableURLRequest alloc] initWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:payloadData];
NSError *error;
NSURLResponse *response;
NSData *data = [[NSURLSession sharedSession] sendSynchronousRequest:storeRequest returningResponse:&response error:&error];
if(error) {
_appleReceipt = [NSArray arrayWithObjects: error, nil];
}
NSError *localError = nil;
//Parsing the response as JSON.
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&localError];
//Getting the latest_receipt_info field value.
_appleReceipt = jsonResponse[#"latest_receipt_info"];
if(SANDBOX_TESTING) NSLog(#"Refresh Apple receipt: %#", _appleReceipt);
}
And after I have the receipt I look through it and look for the correct purchase and extract the expiration date or none if it is a lifetime purchase.
BUT: We have some users getting an error message saying that no apple receipt has been returned (triggered by me, if self.appleReceipt = nil) or that no purchase has been found in this receipt. But very few users and I cannot really see what they share in common and where the error is. In testing I never get an error. I also saw live from one user who made the lifelong purchase that no receipt was returned and I don't know why.
So where is my error? Do I have to refresh the receipt everytime? Or why is sometimes my self.appleReceipt empty? Is my process wrong?
When you verify with the Apple server you do get back the latest updates, including any cancellations or expiration.
Sometimes the receipt may take a while to generate after a purchase, which is why occasionally you will see null receipt data especially if you only check for receipt data immediately after a purchase. Or, it could be even possibly be users attempting to hack your application and not really having receipt data.
What you should also be doing is parsing the receipt data Apple returns, looking at the IAP items for your renewable subscriptions to check on when they might be expiring, or for IAP entries for your one-time purchases - you can find a detailed guide on what data is present in the receipt (along with codes you might encounter) here:
https://www.namiml.com/blog/app-store-verify-receipt-definitive-guide
Note that you should really have a server do this receipt check and processing though, as potentially a hacker could intercept that call to Apple for the receipt verification.

Inapp response goes purchased even the payment is in pending in user purchase history

Do there any chance of getting purchased response from apple for pending transaction. The in-app purchase history of the user shows the transaction in pending state but our paymentcompleted method invokked .
You can check for receipt in the app bundle using
NSData *aData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
if data is present then validate the receipt with app store
for sandbox mode
#"https://sandbox.itunes.apple.com/verifyReceipt"
for production mode
#"https://buy.itunes.apple.com/verifyReceipt"
NSString *encodedReceipt = [aData base64EncodedStringWithOptions:0];
NSError *error;
NSHTTPURLResponse *response = nil;
NSDictionary *parameters = #{#"receipt-data":encodedReceipt,#"password":#"inapp_pwd"};
Http method POST
check this response you will get the status

SKReceiptRefreshRequest asking password everytime

I am using SKReceiptRefreshRequest to validate the receipt from the server. The problem is it is asking me every time password prompt. Can anyone help suggest me a better way to validate the user receipt
Here's what I am doing (i am using refreshReceipt when the app starts)
- (void)refreshReceipt {
SKReceiptRefreshRequest *refresh = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
refresh.delegate = self;
[refresh start];
}
- (void)requestDidFinish:(SKRequest *)request API_AVAILABLE(ios(3.0), macos(10.7)) {
if ([request isKindOfClass:[SKReceiptRefreshRequest class]]) {
NSLog(#"Got a new receipt...");
[self verifyReceipt:self.loadingView :NO :^{
} :^{
[app_delegate jumpToLogin];
}];
}
}
- (void)verifyReceipt :(UIView *)view1 :(BOOL)showHUD : (void (^)(void)) complete : (void (^)(void)) incomplete
{
if (showHUD) {
[UtilityManager showHUD:view1];
}
/* Load the receipt from the app bundle. */
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt) {
/* No local receipt -- handle the error. */
[UtilityManager hideHUD:view1];
incomplete();
return;
}
/* Create the JSON object that describes the request */
NSError *error;
// Verify the recipt
In your case it's asking for a password because sandbox receipt is missing on your device. It's trying to refresh existing receipt, but can't find it. So it's going to get a fresh receipt, that is why it's asking for a password.
In production (when the app is downloaded from the App Store) there will always be a receipt, so it won't require a password.
And why are you using SKReceiptRefreshRequest? It's only required for "Restore purchases" button.
Here is article from our blog: https://blog.apphud.com/receipt-validation/
The receipt_data that is saved when a purchase is made in the device, only have the purchase details. "in_app" has a list of transaction details. The initial receipt will not have cancellation_date for the transaction.
The only was to get cancellation_date for non auto-renewable subscription is by calling SKReceiptRefreshRequest from the code.
It is very inconvenient for the user to enter their password every single time we try to update the receipt. I'm calling SKReceiptRefreshRequest once a week to check for the receipt updates.
I have verified the same with Apple as well by creating a Technical Support Incidents. They don't have a better way to solve this.

Sandbox environment asks for sign in twice

I am testing in-app purchases for the new version of our app, the in-app purchase code was tested before and it was working ok however now it asks for itunes sign-in twice before the purchase confirmation alert. It still works fine and purchases the item correctly after the double sign-in but it is a little bit disturbing. Did anybody have a similar issue lately with sandbox servers?
To give some more info actual transaction verification happens on our server, I'm using RMStore for default dummy verification on client side, it actually does nothing but checks if productID exists in app receipt, the cool side is it refreshes app receipt if it is nil or productID does not exist in the receipt. There are only auto-renewing subscriptions in our app, and I logout from appstore before testing a new purchase.
This roughly how my code looks like:
-(void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
{
RMStoreAppReceiptVerificator *verificator = [RMStoreAppReceiptVerificator new];
[verificator verifyTransaction:transaction success:^{
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
NSString *receiptStr = [receipt base64EncodedStringWithOptions:kNilOptions];
[self sendReceiptToServer:receiptStr];
}
failure:^(NSError *error) {
...
}];
}
break;
case ... : ...
default: ...
}
};
}
To get the login to work you have to logout of the settings > itunes. Then login when the app prompts you too using the test account (created on itunes connect).

iOS Handle IAP download fail on SKDownloadStateFailed state

Our app provides in app purchases to apple hosted extra content which the user downloads, but some users are reporting problems with the download.
It seems to fail halfway through, alert the user and resets all buttons etc to allow the user to buy again (for free as they have already purchased). If the user then tries to re buy or restore purchases in the app it still fails. The following code is what handles a failed state.
-(void) paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads{
for (SKDownload *download in downloads){
switch (download.downloadState){
case SKDownloadStateActive:{
//code removed for post
break;
}
case SKDownloadStateCancelled:{ break; }
case SKDownloadStateFailed:
{
//log the error
NSLog(#"Transaction error: %#", download.transaction.error.localizedDescription);
//let the user know
[[[UIAlertView alloc] initWithTitle:#"Error!" message:[NSString stringWithFormat:#"%# download failed, please try again later", download.contentIdentifier] delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
//post notification - caught in view controller for updating buy buttons etc
[[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperDownloadFailedNotification object:download userInfo:nil];
// This should delete the download assets from the Cache folder.
NSError* error = nil;
[[NSFileManager defaultManager] removeItemAtURL:download.contentURL error:&error];
if(error){
//
}
//finish the transaction
[[SKPaymentQueue defaultQueue] finishTransaction:download.transaction];
break;
}
case SKDownloadStateFinished:{
//code removed for post
break;
}
case SKDownloadStatePaused:{
break;
}
case SKDownloadStateWaiting:{
break;
}
}
}
I've looked around stack overflow and elsewhere and what little examples I can find on in app purchases all do the same as above. Any help or info would be great.
I got similar problem and just found a possible resolution in Apple's FAQ on IAP:
Q: How do I resolve the "You've already purchased this In-App Purchase but it hasn't been downloaded." error message?
A: You are getting the "You've already purchased this In-App Purchase but it hasn't been downloaded." error message because you did not call SKPaymentQueue 's finishTransaction:method in your application. Calling finishTransaction: allows you to remove a transaction from the payment queue.
I think I will ask my IAP engine to check whether there is any IAP is being processed when app exits(my problem occurs when user stopped the app while downloading), if yes, calls finishTransaction:.
Hope it helps.

Resources