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.
Related
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.
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).
I'm having an issue validating receipts for an auto-renewing IAP. Below is a summary of my code for verifying subscriptions. I'm using Parse for a backend, and have some of that code included also. In development mode, everything works perfectly and has no issues. It's live in the App Store, and all of my TestFlight testers, and some real users are experiencing crashes as soon as the app tries to validate. I'm seeing that the crash is coming from the very end of the function when I try to save data back to Parse, it's telling me the keys I'm saving are null. (base64, info, expirationDate)
On my device, I had a sandbox purchase receipt and I get a 21007 response from Apple trying to validate against live URL. When I switch to sandbox url, it validates and works every time.
Also, I know it's not safe to validate purchases this exact way, I perform validation via my server, so no issue there.
I'm trying to figure out if I'm missing a step of if there's something I should be doing differently?
Pseudo code:
Get the receipt from NSBundle
Get the receipt from Parse database
If neither of them exist:
end the function
else:
if Parse receipt exists:
use it, but first just check the expirationDate stored in Parse
else:
use NSBundle receipt for validation,
If expired based on date from Parse:
build request to send to Apple (my server in production)
Get JSON response, and perform switch statement for response codes
Check for errors or expiration
Save new data to Parse // <-- Cause of the crash is here because the keys are null for some users
Here's a piece of actual code:
PFUser *user = [PFUser currentUser];
//Load the receipt from the app bundle
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
//Load the receipt from Parse, already encoded
NSString *saved64 = user[#"base64"];
//Check for at least one instance of a receipt... Either Parse or appBundle
if (!receipt && saved64.length == 0) {
//if (!receipt) {
//No receipt
NSLog(#"No Receipt");
return;
}
//Base 64 encode appBundle receipt
NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];
//String to hold base64 receipt (either from Parse or appBundle)
NSString *temp64;
//See if Parse base64 exists
if (saved64.length == 0) {
//Not a receipt in Parse yet, use appBundle
NSLog(#"Using appBundle receipt.");
temp64 = receipt64;
} else {
//Receipt in Parse, use it
NSLog(#"Using Parse receipt.");
temp64 = saved64;
//Check expiration date stored in Parse
NSDate *parseExpDate = user[#"expirationDate"];
if ([[self todayGMT] compare:parseExpDate] == NSOrderedAscending) {
//Active based on Parse, no need to validate...
NSLog(#"Active based on Parse receipt... No need to validate!");
return;
}
}
//Base 64 encode appBundle receipt
NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];
//Create the request
NSString *sharedSecret = #"[shared-secret]";
NSError *error;
//Request with receipt data and shared secret from iTunesConnect
NSDictionary *requestContents = #{#"receipt-data":receipt64, #"password": sharedSecret};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
if (!requestData) {
//Handle error
NSLog(#"Error: %#", error);
return;
}
//Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:#"https://buy.itunes.apple.com/verifyReceipt"];
//NSURL *sandboxURL = [NSURL URLWithString:#"https://sandbox.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
//Make a connection to the iTunes Store
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
//Error
} else {
//Success
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
//Error
NSLog(#"Error at !jsonResponse: %#", error);
return;
}
NSString *base64 = jsonResponse[#"latest_receipt"];
NSArray *info = jsonResponse[#"latest_receipt_info"];
BOOL isPro;
//Switch statement for subscription status
switch ([jsonResponse[#"status"] intValue]) {
case 21002: {
//The data in the receipt-data property was malformed or missing.
NSLog(#"21002 : The data in the receipt-data property was malformed or missing.");
isPro = NO;
break;
}
case 21003: {
//The receipt could not be authenticated.
NSLog(#"21003 : The receipt could not be authenticated.");
isPro = NO;
break;
}
case 21004: {
//The shared secret you provided does not match the shared secret on file for your account.
NSLog(#"21004 : The shared secret you provided does not match the shared secret on file for your account.");
isPro = NO;
break;
}
case 21005: {
//The receipt server is not currently available.
NSLog(#"21005 : The receipt server is not currently available.");
isPro = NO;
break;
}
case 21006: {
//This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
NSLog(#"21006 : This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.");
isPro = NO;
break;
}
case 21007: {
//This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
NSLog(#"21007 : This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead..");
isPro = NO;
break;
}
case 21008: {
//This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
NSLog(#"21008 : This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead..");
isPro = NO;
break;
}
case 0: {
//Valid and active
NSLog(#"0 : Valid and active subscription.");
isPro = YES;
break;
}
default: {
isPro = NO;
break;
}
}
//Set user info to database (Parse)
user[#"base64"] = base64;
user[#"info"] = info;
user[#"expirationDate"] = expirationDate;
user[#"isPro"] = [NSNumber numberWithBool:isPro];
[user saveEventually];
}
}];
Sandbox URL - https://sandbox.itunes.apple.com/verifyReceipt
Sandbox URL works in developing mode only with the developer certificate, So it gets the response from a server.
Live URL - https://buy.itunes.apple.com/verifyReceipt
Live URL works in distribution mode only with the distribution certificate, So it didn't work in developing mode and couldn't return the response.
So if you want to use the live URL with the debug mode, you should handle the exceptions.
If you use Swift with optional binding, you can parse without crashes.
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?
I want to use Braintree API in my iOS app.
My app is used for renting purpose i.e. requester user has to make payment to the owner of asset he wanted for rent.
I have checked following links :
http://www.youtube.com/watch?v=s7GlgBFM20I
https://www.braintreepayments.com/developers
http://www.youtube.com/watch?v=2y8Tsml6JYo
https://www.braintreepayments.com/braintrust/venmo-touch-screencasts-add-one-touch-payments-to-your-app-in-15-minutes
etc.
But I didn't get any idea where to provide receiver's account details using Braintree or Venmo api, For Example we can pass e-mail of receiver in PayPal iOS sdk, and then PayPal pays the amount to the user registered with that e-mail.
The same thing I am searching in Braintree Payment API.
Any help is greatly appreciated.
Thanks in advance.
I am using below code : (Used from a sample code given by braintree)
/* Get called when user pay on Pay button on screen.
User wil see a form for entering his credit card number, CVV and expiration date. */
-(IBAction)payButtonClicked
{
self.paymentViewController =
[BTPaymentViewController paymentViewControllerWithVenmoTouchEnabled:YES];
self.paymentViewController.delegate = self;
[self presentViewController:self.paymentViewController animated:YES completion:nil];
}
// When a user types in their credit card information correctly, the BTPaymentViewController sends you
// card details via the `didSubmitCardWithInfo` delegate method.
//
// NB: you receive raw, unencrypted info in the `cardInfo` dictionary, but
// for easy PCI Compliance, you should use the `cardInfoEncrypted` dictionary
// to securely pass data through your servers to the Braintree Gateway.
- (void)paymentViewController:(BTPaymentViewController *)paymentViewController
didSubmitCardWithInfo:(NSDictionary *)cardInfo
andCardInfoEncrypted:(NSDictionary *)cardInfoEncrypted {
[self savePaymentInfoToServer:cardInfoEncrypted]; // send card through your server to Braintree Gateway
}
// When a user adds a saved card from Venmo Touch to your app, the BTPaymentViewController sends you
// a paymentMethodCode that you can pass through your servers to the Braintree Gateway to
// add the full card details to your Vault.
- (void)paymentViewController:(BTPaymentViewController *)paymentViewController
didAuthorizeCardWithPaymentMethodCode:(NSString *)paymentMethodCode {
// Create a dictionary of POST data of the format
// {"payment_method_code": "[encrypted payment_method_code data from Venmo Touch client]"}
NSMutableDictionary *paymentInfo = [NSMutableDictionary dictionaryWithObject:paymentMethodCode
forKey:#"payment_method_code"];
[self savePaymentInfoToServer:paymentInfo]; // send card through your server to Braintree Gateway
}
#define SAMPLE_CHECKOUT_BASE_URL #"http://venmo-sdk-sample-two.herokuapp.com"
//#define SAMPLE_CHECKOUT_BASE_URL #"http://localhost:4567"
// Pass payment info (eg card data) from the client to your server (and then to the Braintree Gateway).
// If card data is valid and added to your Vault, display a success message, and dismiss the BTPaymentViewController.
// If saving to your Vault fails, display an error message to the user via `BTPaymentViewController showErrorWithTitle`
// Saving to your Vault may fail, for example when
// * CVV verification does not pass
// * AVS verification does not pass
// * The card number was a valid Luhn number, but nonexistent or no longer valid
- (void) savePaymentInfoToServer:(NSDictionary *)paymentInfo {
NSURL *url;
if ([paymentInfo objectForKey:#"payment_method_code"]) {
url = [NSURL URLWithString: [NSString stringWithFormat:#"%#/card/payment_method_code", SAMPLE_CHECKOUT_BASE_URL]];
} else {
url = [NSURL URLWithString: [NSString stringWithFormat:#"%#/card/add", SAMPLE_CHECKOUT_BASE_URL]];
}
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
// You need a customer id in order to save a card to the Braintree vault.
// Here, for the sake of example, we set customer_id to device id.
// In practice, this is probably whatever user_id your app has assigned to this user.
NSString *customerId = [[UIDevice currentDevice] identifierForVendor].UUIDString;
[paymentInfo setValue:customerId forKey:#"customer_id"];
request.HTTPBody = [self postDataFromDictionary:paymentInfo];
request.HTTPMethod = #"POST";
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *body, NSError *requestError)
{
NSError *err = nil;
if (!response && requestError) {
NSLog(#"requestError: %#", requestError);
[self.paymentViewController showErrorWithTitle:#"Error" message:#"Unable to reach the network."];
return;
}
NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:body options:kNilOptions error:&err];
NSLog(#"saveCardToServer: paymentInfo: %# response: %#, error: %#", paymentInfo, responseDictionary, requestError);
if ([[responseDictionary valueForKey:#"success"] isEqualToNumber:#1]) { // Success!
// Don't forget to call the cleanup method,
// `prepareForDismissal`, on your `BTPaymentViewController`
[self.paymentViewController prepareForDismissal];
// Now you can dismiss and tell the user everything worked.
[self dismissViewControllerAnimated:YES completion:^(void) {
[[[UIAlertView alloc] initWithTitle:#"Success" message:#"Saved your card!" delegate:nil
cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
[[VTClient sharedVTClient] refresh];
}];
} else { // The card did not save correctly, so show the error from server with convenenience method `showErrorWithTitle`
[self.paymentViewController showErrorWithTitle:#"Error saving your card" message:[self messageStringFromResponse:responseDictionary]];
}
}];
}
I work at Braintree. If you've got more questions or need more help, please reach out to our support team.
The Braintree iOS SDK docs include a quickstart guide, as well as detailed information about the features of the library.
If you're looking for information specifically on how to make payments between users of your app / web site, you should take a look at the marketplace guide and the Venmo APIs.