How to get transactionID & receipt from MKStoreKit? - ios

I've implemented MKStoreKit successfully and get notifications successfully after purchasing.
I need to get transactionID & receipt simply but I'm not sure how to get transactionID & receipt from response parameter note.
[[NSNotificationCenter defaultCenter] addObserverForName:kMKStoreKitProductPurchasedNotification
object:nil
queue:[[NSOperationQueue alloc] init]
usingBlock:^(NSNotification *note) {
NSLog(#"Purchased/Subscribed to product with id: %#", [note object]);
}];
I get my IAP productID from note parameter, when it is purchased successfully but seems like it doesn't include transactionID & receipt.
I'm pretty sure the callback block should return necessary informations including transactionID & receipt.
Do I think wrong or this library doesn't support this feature?

After struggling, I finally went to RMStore instead of MKStoreKit.
Here are some of my codes.
[[RMStore defaultStore] addPayment:"YOUR_IAP_PRODUCT_ID" success:^(SKPaymentTransaction *transaction) {
NSString* transactionID = transaction.transactionIdentifier;
NSString* receipt = #"";
NSData *data = [NSData dataWithContentsOfURL:[RMStore receiptURL]];
if(data != nil ) {
receipt = [data base64EncodedStringWithOptions: 0];
}
} failure:^(SKPaymentTransaction *transaction, NSError *error) {
NSLog(#"Something went wrong");
}];

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.

Get only new/modified phone contacts from iPhone

I want to import all phone contacts into app first time only and save on server.
Second time wants to imports only new, modified and deleted contacts into an app and sync with contacts saved on the server which should be done according to created/modified date of contacts.
From iOS 9, Apple won't allow to get create/modified date using Contacts Framework
How can I achieve with better approach?
You can save contacts identifier and import date in local storage like SQLite. And when you receive contacts update notification, you can send saved date of respective updated contact and also update date in SQLite with received notification timestamp(date).
You should add observer for change contact list notification like this:
NotificationCenter.default.addObserver(self,selector: #selector(self.addressBookDidChange(_:)), name: NSNotification.Name.CNContactStoreDidChange,object: nil)
And then
#objc func addressBookDidChange(_ notification: Notification){
print(notification.object as Any, notification.userInfo as Any)
//remove observer so far to prevent double method calling when making operations with contacts
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.CNContactStoreDidChange, object: nil)
//processing
//at some point return observer
NotificationCenter.default.addObserver(self,selector: #selector(ContactsListVC.addressBookDidChange(_:)), name: NSNotification.Name.CNContactStoreDidChange,object: nil)
}
There's an undocumented API in CNContactStore - enumeratorForChangeHistoryFetchRequest:error:
I tried to test this method using the following code:
-(void)testChangeHistoryRequest {
CNContactStore *store = [[CNContactStore alloc] init];
[self requestContactsPermissions:store completion:^(BOOL granted) {
if (!granted)
return;
NSData *storeToken = [store currentHistoryToken];
NSLog(#"testChangeHistoryRequest: store token st start - %#", storeToken);
NSError *error;
CNChangeHistoryFetchRequest *req = [[CNChangeHistoryFetchRequest alloc] init];
[req setAdditionalContactKeyDescriptors:#[CNContactGivenNameKey, CNContactFamilyNameKey]];
[req setMutableObjects:YES];
[req setShouldUnifyResults:YES];
[req setStartingToken:storeToken];
CNFetchResult<NSEnumerator<CNChangeHistoryEvent*>*>* res = [store enumeratorForChangeHistoryFetchRequest:req error:&error];
if (res && res.value) {
NSData *token = [res currentHistoryToken];
NSLog(#"token - %#", token);
for (CNChangeHistoryEvent *e in res.value)
NSLog(#"%# - %#", [e class], e);
NSLog(#"token at end - %#", token);
}
}];
}
What I got is that store.currentHistoryToken never changes - it starts and ends with nil value. Also during the iteration, res.currentHistoryToken is always nil.
I also tried to initialize [req setStartingToken:storeToken]; with arbitrary data, but this changed nothing (and didn't even fail).
My guess is that this enumeration is not fully implemented.

RMStore In App Purchase Unknown Product Identifier

I have spent all day trying to get an in app purchase to work in my app, and everything that I have tried to make it work results in a 'Unkown Product Identifier' response from RMStore. I have tried everything on this list: http://troybrant.net/blog/2010/01/invalid-product-ids/
My Code is:
if ([RMStore canMakePayments]) {
_products = #[#"com.afterdark.afterdark.usersaleslisting"];
[[RMStore defaultStore] requestProducts:[NSSet setWithArray:_products] success:^(NSArray *products, NSArray *invalidProductIdentifiers) {
NSLog(#"Request Success");
NSString *productID = _products[0];
SKProduct *product = [[RMStore defaultStore] productForIdentifier:productID];
NSLog(#"IAP ID: %#",productID);
NSLog(#"IAP TITLE: %#",product.localizedTitle);
NSLog(#"IAP PRICE: %#",[RMStore localizedPriceOfProduct:product]);
[[RMStore defaultStore] addPayment:productID success:^(SKPaymentTransaction *transaction) {
NSLog(#"Payment Succes");
} failure:^(SKPaymentTransaction *transaction, NSError *error) {
NSLog(#"Payment Failed: %#",error.localizedDescription);
}];
} failure:^(NSError *error) {
NSLog(#"Request Failed: %#",error.localizedDescription);
}];
}
Itunes Connect - In App Purchases:
Xcode - In App Purchases Capability
I have completely run out of ideas of how to get this to work, anyone have any ideas? Any help will be appreciated. Thank you in advance.
So, I have finally got this working. I contacted apple and it turns out the only reason it wasn't working is because something had gone wrong on their servers when I created the IAP in iTunes Connect, they promptly resolved the issue.

iOS Receipt Validation - Auto Renewing Subscription

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.

Client side IOS to Server using Braintree Payment Process

I'm using Braintree for Payment process in my application
[BTPaymentViewController paymentViewControllerWithVenmoTouchEnabled:NO];and use this method for encryption
`
(void)paymentViewController:(BTPaymentViewController *)paymentViewController
didSubmitCardWithInfo:(NSDictionary *)cardInfo
andCardInfoEncrypted:(NSDictionary *)cardInfoEncrypted {
NSDictionary *dict=[self encryptFormData:cardInfo];
[self savePaymentInfoToServer:dict];
}
-(NSDictionary *) encryptFormData:(NSDictionary *) formData {
BTEncryption *braintree = [[BTEncryption alloc] initWithPublicKey: PUBLIC_KEY];
NSMutableDictionary *encryptedParams = [[NSMutableDictionary alloc] init];
[formData enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) {
[encryptedParams setObject: [braintree encryptString: object] forKey: key];
}];
return encryptedParams;
}
call to this method to post the data to localhost server for testing
- (void) savePaymentInfoToServer:(NSDictionary *)paymentInfo {
NSURL *url = [NSURL URLWithString: [NSString stringWithFormat:#"%#/card", 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;
AppDelegate *appdelegate=(AppDelegate *) [[UIApplication sharedApplication]delegate];
[paymentInfo setValue:appdelegate.referenceId forKey:#"bookingRefId"];
[paymentInfo setValue:appdelegate.passengerId forKey:#"passengerId"];
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 *<b>responseDictionary</b> = [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];
}];
} 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]];
}
}];
}`
contain responseDictionary is null and error is null how to fix the issue can any one help me
where are you sending the paymentInfo dictionary to (i.e. what is SAMPLE_CHECKOUT_BASE_URL)? The example project built by Braintree simulates a backend as if you had one yourself. You will want to replace that URL with your backend's URL.
The BTPaymentViewController provides a client-side credit card checkout page, but your backend still has to execute the transaction. For your backend to execute that transaction, you'll have to send that paymentInfo dictionary to your servers.
If you haven't yet built a backend for your iOS app, you can quickly get set up and approved in minutes with Braintree to process your payments.

Resources