I believe that to say that receipt verification help from Apple is obfuscated is an understatement.
Somehow I have been able to put some code together that does not use OpenSSL or ASN1 which SEEMS TO WORK to let me get access to the Receipt Fields as readable strings for all receipts for a bundle (including the most current which may not have been even generated by the current device).
This is a work in process, as you can see by the todo's, but could someone tell me why I should not use this method because it just seems too easy from all that I have read on the subject?
Also can anyone help me with the todo's? Like what I need to do at todo1 and 2 (I think i can handle 3, 4 and 5)?
In my case I am scanning the 'latest_receipt_info' receipts from most recent to earliest until I find the in-app product_id I am interested in and then determining its expired status from 'expires_date_ms'. Is that the proper way to determine current expired status of a product_id?
Anyway here it is:
NSURL *storeURL;
exms = [[NSUserDefaults standardUserDefaults] doubleForKey:#"exms"]; //globally defined elsewhere
[[NSUserDefaults standardUserDefaults] synchronize];
NSDate *today = [NSDate date];
int64_t nowms = 1000*[today timeIntervalSince1970];
if (nowms> exms){ //todo maybe give a week grace here
isSubscribed= NO; //globally defined elsewhere
// 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. */ } //todo1
/* ... Now Send the receipt data to server ... */
// Create the JSON object that describes the request
NSError *error;
NSDictionary *requestContents = #{#"receipt-data": [receipt base64EncodedStringWithOptions:0],
#"password": #"put your shared secret here"};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
if (!requestData) { /* ... Handle error ... */ } //todo2
NSString *file=[NSHomeDirectory() stringByAppendingPathComponent:#"iTunesMetadata.plist"];
if ([[NSFileManager defaultManager] fileExistsAtPath:file]) {
// probably a store app
storeURL = [NSURL URLWithString:#"https://buy.itunes.apple.com/verifyReceipt"]; //todo3 test this for sure rather than ifdef debug
}else{
storeURL = [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 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 ... */ //todo4
} else {
int64_t edms= 0;
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) { /* ... Handle error ...*/ } //todo5
// Get object from the root object
NSArray *dictionaryObject = (NSArray *)[jsonResponse objectForKey:#"latest_receipt_info"];
NSDictionary *rcpt;
for (int i=[dictionaryObject count]-1;i>-1;i--){
rcpt= [dictionaryObject objectAtIndex:i];
NSString *pid= [rcpt objectForKey:#"product_id"];
if ([pid isEqualToString:#"put your in-app purchase you are interested in here"]){
edms= [[rcpt objectForKey:#"expires_date_ms"] longLongValue];
break;
}
}
NSDate *today = [NSDate date];
int64_t nowms = 1000*[today timeIntervalSince1970];
if (nowms> edms){
isSubscribed= NO;
}else{
isSubscribed= YES;
}
exms= edms;
[[NSUserDefaults standardUserDefaults] setDouble:exms forKey:#"exms"];
[[NSUserDefaults standardUserDefaults] synchronize];
if (isSubscribed== YES) _isexpiredView.hidden= true;
}
}];
}else{
isSubscribed= YES;
}
The mechanical steps of receipt validation aren't hard and your implementation addresses these. Doing it securely is more difficult as you cannot trust the end-user device.
The first issue I see is that you are storing the expiration date in NSUserDefaults and you only validate the receipt if "now" is after the expiration date. So I can simply put an expiration date far into the future into NSUserDefaults and you will never expire the subscription nor check to see if the expiration date is valid. Using an encrypted value and storing in the keychain would make it more secure.
Secondly you are validating the receipt directly from the device to the Apple server. From the Receipt Validation Programming Guide
Use a trusted server to communicate with the App Store. Using your own
server lets you design your app to recognize and trust only your
server, and lets you ensure that your server connects with the App
Store server. It is not possible to build a trusted connection between
a user’s device and the App Store directly because you don’t control
either end of that connection.
What this is saying is that by connecting directly from your device you make it easy for someone to spoof the Apple Server. If you validated that the receipt was signed by Apple then this would be harder, but as it is you blindly trust the expiration date in the receipt.
Your approach is OK as long as you trust your users are honest.
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.
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
I'm developing an iOS app that needs to read User data from MS Azure Active Directory.
I have successfully followed some examples on iOS app from the MS Azure documentation and successfully brought up their authentication page and have the user signed in. What I get back is some user data in the form of a ADUserInformation object.
Here's is the code I have:
NSString *authority = #"https://login.microsoftonline.com/a5960f61-0bf9-4bf6-96cd-98c61d30XXXX/federationmetadata/2007-06/federationmetadata.xml";
NSString *resourceId = #"74cd2559-0389-4871-9904-bc767d71XXXX"; // (server)
NSString *clientId = #"c8a956a7-84b7-4050-875c-896aab6bXXXX"; //ios-client (us)
NSURL *redirectUri = [[NSURL alloc]initWithString:#"https://XXXXevents.azurewebsites.net/.auth/login/done"];
ADAuthenticationError *error;
ADAuthenticationContext * authContext = [ADAuthenticationContext authenticationContextWithAuthority:authority error:&error];
//authContext.parentController = parent;
[ADAuthenticationSettings sharedInstance].enableFullScreen = YES;
[authContext acquireTokenWithResource:resourceId
clientId:clientId
redirectUri:redirectUri
completionBlock:^(ADAuthenticationResult *result) {
if (result.status != AD_SUCCEEDED) {
NSLog(#"%#", result);
return;
}
else {
//save all of this information into core data
NSDictionary * payload = #{#"access_token" : result.tokenCacheItem.accessToken};
NSLog(#"%#", payload);
//#"aad"
//#"windowsazureactivedirectory"
[[QSActivityService defaultService].client loginWithProvider: #"aad"
token: payload
completion: ^(MSUser * _Nullable user, NSError * _Nullable error) {
NSLog(#"loginWithProvider-------");
if(!error) {
NSLog(#"YAY! %s - user: %# ", __FUNCTION__, user.userId);
ADUserInformation * temp = result.tokenCacheItem.userInformation;
[[CoreDataStack defaultStack] updateUserDetailFamilyName:temp.allClaims[#"family_name"]
version:temp.allClaims[#"ver"]
email:temp.allClaims[#"email"]
nbf:temp.allClaims[#"nbf"]
exp:temp.allClaims[#"exp"]
givenName:temp.allClaims[#"given_name"]
idp:temp.allClaims[#"idp"]
ipaddr:temp.allClaims[#"ipaddr"]
iss:temp.allClaims[#"iss"]
oid:temp.allClaims[#"oid"]
typ:temp.allClaims[#"typ"]
sub:temp.allClaims[#"sub"]
amr:temp.allClaims[#"amr"]
aud:temp.allClaims[#"aud"]
alg:temp.allClaims[#"alg"]
iat:temp.allClaims[#"iat"]
tid:temp.allClaims[#"tid"]
name:temp.allClaims[#"name"]
uniqueName:temp.allClaims[#"unique_name"]];
//other code, no problems here
MS Graph API
However, I would like access profile images, and all the other data. I have read that MS Graph API provides it, but I'm not sure how and where I would put the token.
Do I use the token from result.tokenCacheItem.accessToken? If so, in the header? or body?
Or do I simply hit up graph.windows.com twice. First time to get the Authentication Token, and second time for the data?
I have read a lot of documentation and none of them works as I keep getting the Token Missing or Malformed error message.
My Graph API code looks like this:
-(void)getUsersUsingAccessToken:(NSDictionary*)token completion:(void (^) (void))completion {
NSString * tenant = #"a5960f61-0bf9-4bf6-96cd-98c61d306f12";
NSString * accessToken = token[#"access_token"];
NSString * urlString = [NSString stringWithFormat: #"https://graph.windows.net/%#/tenantDetails?api-version=1.6", tenant];
NSString * httpVerb = #"POST";
//build an info object and convert to json
NSDictionary * bodyFormDict
= [NSDictionary dictionaryWithObjectsAndKeys:
#"client_credentials", #"grant_type",
#"https://graph.windows.net", #"resource",
#"c8a956a7-84b7-4050-875c-896aab6xxxx", #"client_id",
#"XLlZl69aUKiQTo4dpeiprItm+LYbDtpt6e9dn0bxxxx", #"client_secret",
nil];
NSError *error = nil;
//1st step
NSData * jsonInputData = [NSJSONSerialization dataWithJSONObject:bodyFormDict
options:NSJSONWritingPrettyPrinted
error:&error];
//2nd step
NSString * httpBodyString = [[NSString alloc]
initWithData:jsonInputData
encoding:NSUTF8StringEncoding];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.allowsCellularAccess = YES;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
request.HTTPMethod = httpVerb;
[request setValue: #"application/json; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setValue: accessToken forHTTPHeaderField:#"Authorization: Bearer"];
[request setHTTPBody:[httpBodyString dataUsingEncoding:NSUTF8StringEncoding]];
//asynchronous
NSURLSessionDataTask * getDataTask = [self.session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
//other code
}
If someone can provide working code sample using objective c to successfully retrieve data from the MS Graph API, it would be a great help.
Thanks for your time!
I believe the problem you are having is that the http header field isn't set correctly. Try this -
NSString *authValue = [NSString stringWithFormat:#"Bearer %#", accessToken];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
I found my answers in MS MSDN's starter projects and code samples
https://msdn.microsoft.com/en-us/office/office365/howto/starter-projects-and-code-samples
The specific project that helped me is this:
https://github.com/OfficeDev/O365-iOS-Microsoft-Graph-Profile
As you are going through that sample keep in mind:
Replace INSERT-AUTHORITY-HERE - the name of the tenant in which you provisioned your application. The format should be https://login.windows.net/(YourAzureUserName).onmicrosoft.com
I have found that
https://login.microsoftonline.com/YourAzureAccountID/federationmetadata/2007-06/federationmetadata.xml
also works
Replace INSERT-RESOURCE-ID-HERE - the ID for your mobile app backend. This is the Web API service app ID. NOT the native client iOS app.
Replace INSERT-CLIENT-ID-HERE - the client ID you copied from your iOS NATIVE client application. NOT the Web API service app.
Replace INSERT-REDIRECT-URI-HERE - your site’s /.auth/login/done endpoint, using the HTTPS scheme. This value should be similar to
#"https://XXXXXXXXXX.azurewebsites.net/.auth/login/done"
IF you have trouble importing the ADAL framework...
http://shanghaiseagull.com/index.php/2016/05/11/import-another-project-framework-into-your-project/
library can be found here: https://github.com/AzureAD/azure-activedirectory-library-for-objc
Hope it helps someone starting out...and please let me know if I can be of further help.
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'm working with an app that requests data from an OAuth2.0 protected server. When I use the GTM OAuth Library to retrieve data, the program continues to run while the data is being downloaded in the background. I need some sort of mechanism to either force my application to wait until the didFinishWithData selector is called,or I need a way to notify my ViewController of the download's completion, so I can then utilize the data immediately.
I've tried conditional blocks, but those aren't doing it for me. I've also tried polling the object whose data I'm interested in, but if I do that, the data never seems to download. I've heard I can somehow utilize the Notification Center to accomplish this task, so I'll look more into that while I'm waiting for replies here.
Here is basically what is going on:
-(void) getAlert{
// Define the URL of the API module we'd like to utilize.
NSURL *url = [[NSURL alloc] initWithString:#"https://access.active911.com/interface/open_api/api/alerts"];
// Constructs a an HTTP request object to send to the server in order to obtain data.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:#"1" forHTTPHeaderField:#"alert_days"];
// This fetcher sends the request along with the authentication header in a recognizable manner.
GTMHTTPFetcher *fetcher = [[GTMHTTPFetcher alloc] initWithRequest:request];
// Attach the OAuth credentials for the fetcher's use.
[fetcher setAuthorizer:auth];
// Execute the operation.
[fetcher waitForCompletionWithTimeout:10];
NSLog(#"About to get alert");
[fetcher beginFetchWithDelegate:self didFinishSelector:#selector(responseHandler:finishedWithData:finishedWithError:)];
NSLog(#"got alert");
}
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
// Retrieve the server data in a usable object
// All that's being done here is conversion to an NSDictionary
// followed by the creation of subdictionaries from that dictionary
// until our final value can be picked directly out of the resulting dict
NSData *jsonData = [[NSData alloc] initWithData:data];
NSError *dictError;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:jsonData //1
options:kNilOptions
error:&dictError];
NSDictionary *token = [json objectForKeyedSubscript:#"message"];
NSArray *alerts = [token objectForKeyedSubscript:#"alerts"];
NSDictionary *alertData = alerts[0];
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
NSString *city = [alertData objectForKeyedSubscript:#"city"];
NSLog(#"Map code: '%#' with city '%#' and access token %#", mapCode, city, accessToken);
}
And I need to pass the mapCode to my view controller.
Thanks for the help!
First off, please rethink about having the UI halt while you fetch results from the server. This can create an extremely bad UX for the app and only should be done if absolutely necessary.
Second, does your responseHandler method work? And do you only need mapCode in the VC that responseHandler is in?
If so, you don't even need to use Notifications. Simply do:
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
...
...
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
[self updateVCWithMapCode:mapCode];
}
That will call the method after the response has been received. Passing it explicitly too so you don't need to have mapCode be a property as well.