I have been trying to upload a new version of my app containing non-renewing In-app Purchases with no success. It has been revoked for almost two months and i cannot find the problem.
When i'm testing with a sandbox account the purchase goes to my server, i authenticate the receipt and then update my user's status. But when my app goes to review, the reviewer says that my app doesn't deliver user's paid content, but i get not single attempt on my server.
I have made some changes on my Objective-C code hoping that maybe the error could be the timeout, which now i changed to 45.0 seconds. How long it is supposed to be?
I also made some changes to my server code that check if the purchase have been made by a sandbox or production account.
So... this is the method called after SKPaymentTransactionStatePurchased.
#pragma mark pagamento
-(void)completarTransacao:(SKPaymentTransaction *)transacao
{
[SVProgressHUD dismiss];
receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
if (!receipt)
{
receipt = transacao.transactionReceipt;
}
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[SVProgressHUD showWithStatus:NSLocalizedString(#"Efetuando assinatura...", nil)];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#assinaturaplano/", [[NSDictionary dictionaryWithContentsOfFile:configuracoes_plist] objectForKey:#"Dominio"]]] cachePolicy:nil timeoutInterval:45.0];
[request setHTTPMethod:#"POST"];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField: #"Content-Type"];
[request setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[NSURL URLWithString:#"http://temp"]]]];
NSString *postString = [NSString stringWithFormat:#"receipt=%#&transactionIdentifier=%#&origem=%#", [receipt.base64Encoding urlencode], transacao.transactionIdentifier, [[NSDictionary dictionaryWithContentsOfFile:[home_documents stringByAppendingPathComponent:#"compra"]] objectForKey:#"origem"]];
[request setHTTPBody:[NSData dataWithBytes:[postString UTF8String] length:postString.length]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *erro)
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[SVProgressHUD dismiss];
if ([(NSHTTPURLResponse*)response statusCode] == 200 || [(NSHTTPURLResponse*)response statusCode] == 201)
{
// SUBSCRIPTION CONFIRMED
[SVProgressHUD showSuccessWithStatus:NSLocalizedString(#"Assinatura efetuada com sucesso!", nil)];
[[NSNotificationCenter defaultCenter] postNotificationName:#"atualizarGold" object:nil];
}
else
{
// SUBSCRIPTION NOT CONFIRMED
[SVProgressHUD showErrorWithStatus:NSLocalizedString(#"Assinatura não efetuada. Tente novamente.", nil)];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transacao];
}];
}
My purchase method always goes to else when in review.
Review response
Reasons
2.2: Apps that exhibit bugs will be rejected
----- 2.2 ----- We found that your app exhibited one or more bugs, when reviewed on iPad running iOS 8 and iPhone 5s running iOS 8, on
both Wi-Fi and cellular networks, which is not in compliance with the
App Store Review Guidelines. In App Purchase does not complete. After
users tap on the In App Purchase, enter the Apple ID and password and
confirm the purchase, an error message is produced. The steps to
reproduce are:
1. launch app
2. sign in with the provided credentials
3. select 'Gold Membership'
4. tap '7 days'
5. enter the user's Apple ID and password
6. confirm purchase
7. error message appears
What am i doing wrong? Why does it work only though sandbox?
To find the receipt info i did the following:
// Transaction: SKPaymentTransaction
// storeURL: Sandbox - https://sandbox.itunes.apple.com/verifyReceipt | Production - https://buy.itunes.apple.com/verifyReceipt
-(void)validateTransaction:(SKPaymentTransaction *)transaction storeURL:(NSString *)url
{
receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
if (!receipt)
{
receipt = transacao.transactionReceipt;
}
NSError *error;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:[NSJSONSerialization dataWithJSONObject:#{#"receipt-data": [receipt base64EncodedStringWithOptions:0], #"password" : #"yourpassword"} options:0 error:&error]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
if ([[jsonResponse objectForKey:#"status"] intValue] == 0)
{
NSDictionary *purchaseInfo;
NSArray *inApp = [[jsonResponse objectForKey:#"receipt"] objectForKey:#"in_app"];
if (inApp)
{
for (NSDictionary *receptDictionary in inApp)
{
if ([[receptDictionary objectForKey:#"transaction_id"] isEqualToString:transacao.transactionIdentifier])
{
purchaseInfo = receptDictionary;
break;
}
}
}
// The recent has been found
// Send it to your server
}
else
{
switch ([[jsonResponse objectForKey:#"status"] intValue])
{
case 21003:
// Receipt not authenticated
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case 21005:
// Server not available
break;
case 21007:
// Sandbox receipt send it to sandbox server
[self validateTransaction:transaction storeURL:#"https://sandbox.itunes.apple.com/verifyReceipt"];
break;
case 21008:
// Production receipt send it to production
[self validateTransaction:transaction storeURL:#"https://buy.itunes.apple.com/verifyReceipt"];
break;
}
}
}
else
{
// It was not possible to connect AppStore
}
}];
}
Related
How to get latest receipt information of a renewable in app product in ios and how to check whether receipt is expire or not?
My code is :
- For getting receipt
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
{
hide_HUD
SKPaymentTransaction *transaction = transactions.lastObject;
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
{
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
NSLog(#"Order id ======>> %#",transaction.transactionIdentifier);
NSData *recData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString* receiptString = [[NSString alloc] initWithData:recData encoding:NSUTF8StringEncoding];
[self getReceiptFromAppStore:recData Transaction:transaction isBackground:NO];
break;
}
case SKPaymentTransactionStateFailed:
NSLog(#"Purchase failed ");
break;
case SKPaymentTransactionStateRestored:
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
break;
default:
//NSLog(#"Purchase failed ");
break;
}
}
For getting receipt
-(void)getReceiptFromAppStore:(NSData*)ReceiptData Transaction:(SKPaymentTransaction*)AutoTransaction isBackground:(BOOL)isBackground
{
NSData *receipt=ReceiptData; // Sent to the server by the device
// Create the JSON object that describes the request
NSError *error;
NSDictionary *requestContents = #{
#"receipt-data": [receipt base64EncodedStringWithOptions:0],
#"password" : #“*********************”
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { /* ... Handle error ... */ }
// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:#"https://sandbox.itunes.apple.com/verifyReceipt"]; //for Testing
// NSURL *storeURL = [NSURL URLWithString:#"https://buy.itunes.apple.com/verifyReceipt"];//for live
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error == nil && [jsonResponse valueForKey:#"latest_receipt_info"] != nil && [[jsonResponse valueForKey:#"latest_receipt_info"] count] > 0) {
/*
What I have to do here to check receipt is expire.
My goal is to user can not redirect to my
app’s home page if he unsubscribe.
For example if user purchased 1 month .
renewable plan on 20-1-2017 and on 25-1-2017
he/she unsubscribe for that plan and after 1 month (on 22-2-2017),
if he/she open my app , he should not get
redirected to my app’s home page.
*/
}
else{
NSLog(#"%#",error);
}
}
}];
}
I have got receipt but I don't know how to get data from receipt and how to use it for preventing user to go to app's home page.
Kindly read about receipt fields, You will have a all information like expire, cancelled by apple, billing info, cancellation and so on..
Link
You have to access latest object from latest_receipt_info array and check for field expires_date
The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT.
ASN.1 Field Type 1708
ASN.1 Field Value IA5STRING, interpreted as an RFC 3339 date
JSON Field Name expires_date
JSON Field Value string, interpreted as an RFC 3339 date
This key is only present for auto-renewable subscription receipts. Use this value to identify the date when the subscription will renew or expire, to determine if a customer should have access to content or service. After validating the latest receipt, if the subscription expiration date for the latest renewal transaction is a past date, it is safe to assume that the subscription has expired.
I have implemented Inapp purchase in my app and validating receipt but I have problem with validate receipt when i am on offline.How can i do that?
Following is my code to validate when my interent is connected.
-(void)refreshRecipt
{
NSError *error;
_isUserActive = NO;
NSURL *recieptUrl = [[NSBundle mainBundle]appStoreReceiptURL];
NSError *recieptError;
BOOL isPresent = [recieptUrl checkResourceIsReachableAndReturnError:&recieptError];
if (!isPresent)
{
SKReceiptRefreshRequest *ref = [[SKReceiptRefreshRequest alloc]init];
ref.delegate =self;
[ref start];
return;
}
NSData *reciptData = [NSData dataWithContentsOfURL:recieptUrl];
if (!reciptData)
{
return;
}
dicPayload = [NSMutableDictionary dictionaryWithObject:[reciptData base64EncodedStringWithOptions:0] forKey:#"receipt-data"];
[dicPayload setObject:#"21f843e264474b68b3a81c6b7ca19938" forKey:#"password"];
NSData *requestData = [NSJSONSerialization dataWithJSONObject:dicPayload
options:0
error:&error];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:verifyRecieptURL]];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:requestData];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
NSLog(#"error");
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if ([jsonResponse objectForKey:#"latest_receipt_info"])
{
NSArray *array = [jsonResponse objectForKey:#"latest_receipt_info"];
NSLog(#"%#",array);
NSDictionary *latestDetail = [array lastObject];
if ([latestDetail objectForKey:#"is_trial_period"])
{
if ([[latestDetail objectForKey:#"is_trial_period"] isEqualToString:#"true"])
{
_isFreeTrialActive = YES;
}
else
{
_isFreeTrialActive = NO;
}
_isUserActive = [self calculateCurrentSubscriptionActive:[latestDetail objectForKey:#"expires_date_ms"]];
if (_isUserActive)
{
NSLog(#"User is active");
_SubscriptionActive = YES;
}
else
{
_SubscriptionActive = NO;
NSLog(#"User is not active");
}
}
else
{
NSLog(#"no purachase done,first time user!");
}
}
}
}];
}
please help me to sort out this.
You can validate you receipt locally, this is how you would do it according to apple documentation:
Validate the Receipt
To validate the receipt, perform the following tests, in order:
1- Locate the receipt.
If no receipt is present, validation fails.
2- Verify that the receipt is properly signed by Apple.
If it is not signed by Apple, validation fails.
3- Verify that the bundle identifier in the receipt matches a hard-coded constant containing the CFBundleIdentifier value you expect in the Info.plist file.
If they do not match, validation fails.
4- Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file.
If they do not match, validation fails.
5- Compute the hash of the GUID as described in Compute the Hash of the GUID.
If the result does not match the hash in the receipt, validation fails.
If all of the tests pass, validation passes.
In order to do so you would first need to decrypt the receipt. For this you can use a library like RMStore. Using this library you can do something like:
- (RMAppReceipt *)validReceipt {
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
if (!receipt)
//No receipt
return nil;
if (![receipt.bundleIdentifier isEqualToString:#"Your bundle ID"]) {
//Receipt is invalid
return nil;
}
if (![receipt verifyReceiptHash]) {
//Receipt is invalid
return nil;
}
//Receipt is valid
return receipt;
}
RMAppReceipt contains all information from the receipt.
My app contains consumable IAP products, returns more than one transactions when I call validation receipt with this code:
[[NSBundle mainBundle] appStoreReceiptURL];
Is there any way to return only last transaction?
Is it related about restoring transactions?
I checked this Multiple receipt count for restoreCompletedTransaction inapp purchasing and this iOS in-app-purchase restore returns many transactions.
I tried to restore all purchases but it didn't work.
I'm using these lines for calling receipt:
- (void) checkReceipt {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if(!receipt) {
}
NSError *error;
NSDictionary *requestContents = #{#"receipt-data": [receipt base64EncodedStringWithOptions:0]};
NSLog(#"requestContents:%#", requestContents);
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { }
NSURL *storeURL = [NSURL URLWithString:#"https://sandbox.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
} else {
}
}];
}
Note: This app supports iOS 8+.
It's not related to restoring transactions, it is because apple responds with array of all in-app transactions made by the user when making a validation request. The same information is contained in the receipt if you decode it locally.
If you are looking for the last transaction made you can sort the array ascending by the purchase_date_ms and take the last one.
My objective-c is not so hot so I can't help you with sorting but this document may help: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Collections/Articles/Arrays.html
I want to load the app purchase receipt on app launch. How can I simulate an app purchase (not an In-App Purchase, but an actual App purchase) so that I'll have a receipt? (I'm trying to go from paid to freemium).
I'm using this code to load the receipts
(BOOL)isAppPreviouslyPurchased {
BOOL wasPreviouslyPurchased = false;
// Load the receipt from the app bundle.
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
if (receiptData) {
//read purchase version from receipt
NSDictionary *receipt = [NSJSONSerialization JSONObjectWithData:receiptData options:0 error:nil];
NSString *oldVersion = receipt[#"original_application_version"];
float vFloat = [oldVersion floatValue];
if (vFloat < 1.6) {
wasPreviouslyPurchased = true;
}
}
return wasPreviouslyPurchased;
}
First of all : Refresh your receipt
SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init];
request.delegate = self;
[request start];
Add the SKPaymentTransactionObserver protocol and this method
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
// Call the appropriate custom method for the transaction state.
case SKPaymentTransactionStatePurchasing:
[self showTransactionAsInProgress:transaction deferred:NO];
break;
case SKPaymentTransactionStateDeferred:
[self showTransactionAsInProgress:transaction deferred:YES];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
default:
// For debugging
NSLog(#"Unexpected transaction state %#", #(transaction.transactionState));
break;
}
}
}
Then, when this method will be called, your receipt will be refreshed ;)
Secondly : You must decrypt the receipt
NSData *receipt; // Sent to the server by the device
// Create the JSON object that describes the request
NSError *error;
NSDictionary *requestContents = #{
#"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { /* ... Handle error ... */ }
// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:#"https://buy.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
/* ... Handle error ... */
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) { /* ... Handle error ...*/ }
/* ... Send a response back to the device ... */
}
}];
You can decrypt it with this code, but it's not really recommended by Apple. You should call iTunes from your server.
Then, you can call your method with the response returned by Apple server.
Like this (with locally validation, bad way as Apple said)
NSData *receipt; // Sent to the server by the device
// Create the JSON object that describes the request
NSError *error;
NSDictionary *requestContents = #{
#"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { /* ... Handle error ... */ }
// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:#"https://buy.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
/* ... Handle error ... */
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) { /* ... Handle error ...*/ }
[self isAppPreviouslyPurchased:jsonResponse];
}
}];
-(BOOL)isAppPreviouslyPurchased:(NSDictionary *)receipt {
BOOL wasPreviouslyPurchased = false;
NSString *oldVersion = receipt[#"original_application_version"];
float vFloat = [oldVersion floatValue];
if (vFloat < 1.6) {
wasPreviouslyPurchased = true;
}
return wasPreviouslyPurchased;
}
I am new to receipt validation in iOS. I have implemented in-app purchase successfully. Now I wish to include receipt validation part in this in-app puchase.
My in-app purchase is for 3 products. I want that before purchase of each single product in my app, receipt validation should be performed.
For this I followed the following link: official apple developer tutorial
I managed to get the receipt data, but I am puzzled as what to do after getting the receipt data. How do I send it back to apple server for verification and then start the in-app purchase process?
Following is my code:
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
NSLog(#"transactions count : %d",transactions.count);
BOOL flgIsProductRestorable=NO;
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
{
// show wait view here
//statusLabel.text = #"Processing...";
NSLog(#"Processing...");
}
break;
case SKPaymentTransactionStatePurchased:
{
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[SVProgressHUD dismiss];
// remove wait view and unlock feature 2
//statusLabel.text = #"Done!";
NSLog(#"Success : %# = %#",transaction.payment.productIdentifier,transaction.transactionIdentifier);
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
NSLog(#"%#",receipt);
NSError *error;
NSDictionary *requestContents = #{
#"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { /* ... Handle error ... */ }
// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:#"https://sand.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
/* ... Handle error ... */
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
NSLog(#"response : %#",jsonResponse);
if (!jsonResponse) { /* ... Handle error ...*/ }
/* ... Send a response back to the device ... */
}
}];
[self WS_EbookDetail:transaction.transactionIdentifier];
}...
I have just copy pasted the receipt retrieval code from the apple link I have given in this question.
What do I do next?
Keep in mind Apple recommends that you securely send the receipt data to your server and then call their servers. Calls to Apple's servers are not secure.
You can also do local receipt validation in your app. See Validating Receipts Locally for information on how to do this.
There is also a great WWDC video from 2014 "Preventing Unauthorized Purchases with Receipts" which goes into detail about implementing on device receipt validation.