Trouble getting the original app version that the user installed (receipt validation)? - ios7

I have an app that I recently updated to work with in app purchases. The previous version (paid but with no in app purchases) was 1.0 and the current version is 1.1.
As the in app purchase essentially unlocks all features (which were included in the paid version 1.0), I wanted a way for users that had originally downloaded version 1.0 to be upgraded if they pressed my restore purchases button.
To do this, I first try to restore purchases and if the response to:
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
If this provides me with a queue with a transactions count of 0, I check the receipt to see if the original version installed was 1.0.
The code to get the receipt is as per Apple's documentation
- (void)tryRestoreFromOriginalPurchase
{
// Load the receipt from the app bundle
NSError *error;
NSData *receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
if (receipt == nil) {
[self restoreFromOriginalVersionWithReceipt:nil];
return;
}
// Create the JSON object that describes the request
NSDictionary *requestContents = #{#"receipt-data": [receipt base64EncodedStringWithOptions:0]};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
if (!requestData) {
[self restoreFromOriginalVersionWithReceipt:nil];
return;
}
// 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) {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (jsonResponse) [self restoreFromOriginalVersionWithReceipt:jsonResponse];
else [self restoreFromOriginalVersionWithReceipt:nil];
} else {
[self restoreFromOriginalVersionWithReceipt:nil];
}
}];
}
This then calls the following method:
- (void)restoreFromOriginalVersionWithReceipt:(NSDictionary *)receipt
{
if (receipt == nil) {
// CALL METHOD TO HANDLE FAILED RESTORE
} else {
NSInteger status = [[receipt valueForKey:#"status"] integerValue];
if (status == 0) {
NSString *originalApplicationVersion = [[receipt valueForKey:#"receipt"] valueForKey:#"original_application_version"];
if (originalApplicationVersion != nil && [originalApplicationVersion isEqualToString:#"1.0"]) {
// CALL METHOD TO HANDLE SUCCESSFUL RESTORE
} else {
// CALL METHOD TO HANDLE FAILED RESTORE
}
} else {
// CALL METHOD TO HANDLE FAILED RESTORE
}
}
}
Now this doesn't work. When someone installs version 1.1 and taps restore purchases, it restores successfully when it shouldn't.
I've just realized that in my Info.plist, my CFBundleShortVersionString is 1.1 but my CFBundleVersion is 1.0.
This may be a really daft question, but is the receipt providing an original_application_version of 1.0 (due to the wrong CFBundleVersion) even though the update is versioned 1.1?
So if I release a new update with this corrected to say version 1.2 (for both the CFBundleShortVersionString and CFBundleVersion), will the problem be resolved?
-- UPDATE --
So I just uploaded a new version to the app store with both the CGBundleVersion and CFBundleShortVersionString equal to 1.2. However, I'm still facing the same problem - users downloading version 1.2 for the first time and tapping on restore purchases are being upgraded for free (due to the receipt check outlined above). It seems the original_application_version is always coming to 1.0.
Note: I am downloading the app under a new iTunes account that has not previously downloaded the app.
Here is the receipt I have if I install from the app store and then try to get the receipt through Xcode
2014-08-27 08:46:42.858 AppName[4138:1803] {
environment = Production;
receipt = {
"adam_id" = AppID;
"application_version" = "1.0";
"bundle_id" = "com.CompanyName.AppName";
"download_id" = 94004873536255;
"in_app" = (
);
"original_application_version" = "1.0";
"original_purchase_date" = "2014-08-26 22:30:49 Etc/GMT";
"original_purchase_date_ms" = 1409092249000;
"original_purchase_date_pst" = "2014-08-26 15:30:49 America/Los_Angeles";
"receipt_type" = Production;
"request_date" = "2014-08-26 22:46:42 Etc/GMT";
"request_date_ms" = 1409093202544;
"request_date_pst" = "2014-08-26 15:46:42 America/Los_Angeles";
};
status = 0;
}
Any thoughts?

I stumbled upon the same issue - I converted my app from paid to freemium and tried to use original_application_version in the app receipt to decide who to unlock the new freemium features for. I, too, was unsuccessful.
However, what I found out was that I was using original_application_version incorrectly. The name misled me into thinking that this string corresponds to the version number of the app. On iOS, it does not. original_application_version is actually the build number of the app.
Original Application Version
This corresponds to the value of CFBundleVersion (in iOS) or
CFBundleShortVersionString (in macOS) in the Info.plist file when the
purchase was originally made. In the sandbox environment, the value of this field is always “1.0”.
Source: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
I think this could be the reason why you are getting a number you are not expecting.
Using original_purchase_date, as you did in the end, is a reliable alternative though.

It's been a while since this question was asked, but it raises some very important points:
The original app version field in the receipt corresponds to CFBundleVersion, not CFBundleShortVersionString. In a sandbox (developer build) environment, the value string is always "1.0".
Note that when converting from a paid app to freemium, if an original (paid version) user uninstalls the app, then reinstalls from iTunes, there will be no local receipt. Calling SKPaymentQueue restoreCompletedTransactions will not cause a new receipt to be downloaded if that user has never made an in-app purchase. In this situation, you need ask for a receipt refresh using SKReceiptRefreshRequest and not rely on restore functionality.

Related

how to get checkout details from paddle in mac app?

I have integrated my mac app with paddle this what i followed from documentation and integrated but what i want when user purchases from here i want all the order details.
// Your Paddle SDK Config from the Vendor Dashboard:
NSString* myPaddleProductID = #"12345";
NSString *myPaddleVendorID = #"56791";
NSString* myPaddleAPIKey = #"abc123def345hij678";
// Populate a local object in case we're unable to retrieve data
// from the Vendor Dashboard:
PADProductConfiguration *defaultProductConfig = [[PADProductConfiguration alloc] init];
defaultProductConfig.productName = #"My v4 Product";
defaultProductConfig.vendorName = #"My Company";
// Initialize the SDK Instance with Seller details:
Paddle *paddle = [Paddle sharedInstanceWithVendorID:myPaddleVendorID
apiKey:myPaddleAPIKey
productID:myPaddleProductID
configuration:defaultProductConfig];
// Initialize the Product you'd like to work with:
PADProduct *paddleProduct = [[PADProduct alloc] initWithProductID:myPaddleProductID productType:PADProductTypeSDKProduct configuration:nil];
// Ask the Product to get it's latest state and info from the Paddle Platform:
[paddleProduct refresh:^(NSDictionary * _Nullable productDelta, NSError * _Nullable error) {
// Launch the "Product Info" gatekeeper UI with buy, activate, etc:
[paddle showProductAccessDialogWithProduct:paddleProduct];
however in documentation for custom implementation by using below block we can get but i want by using showProductAccessDialogWithProduct
[paddle showCheckoutForProduct:paddleProduct options:nil checkoutStatusCompletion:^(PADCheckoutState state, NSDictionary * _Nullable checkoutData) {
// Examine checkout state to determine the checkout result
}];
Any Suggestions ?
Thanks In Advance !!
So i got the answer till V4.0.9 we cannot get the details it is included after v4.0.10.
All you need is PADProductDelegate
-(void)productPurchased:(PADCheckoutData *)checkoutData
using this delegate method you can get checkoutdata and if you need order details
get checkout_id from checkoutdata and pass it on below api.
https://checkout.paddle.com/api/1.0/order?checkout_id=xxxxxxxxx

Why is NSURL's NSURLDocumentIdentifierKey (almost) always nil?

OSX Yosemite introduced a very handy attribute on NSURL: NSURLDocumentIdentifierKey.
Quoting from the documentation:
NSURLDocumentIdentifierKey
The document identifier returned as an NSNumber (read-only).
The document identifier is a value assigned by the kernel to a file or directory. This value is used to identify the document regardless of where it is moved on a volume. The identifier persists across system restarts. It is not transferred when the file is copied, but it survives "safe save” operations. For example, it remains on the path to which it was assigned, even after calling the replaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error: method. Document identifiers are only unique within a single volume. This property is not supported by all volumes.
Available in OS X v10.10 and iOS 8.0.
Unfortunately, the value seems to be mostly nil (except rare examples that seem completely disconnected one to the other).
In particular, this code will throw an exception at the last line (tested on Yosemite 10.10.3):
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *attributesFlags = #[NSURLNameKey, mNSURLDocumentIdentifierKey];
NSDirectoryEnumerator *en = [fileManager enumeratorAtURL:[NSURL URLWithString:NSHomeDirectory()]
includingPropertiesForKeys:attributesFlags
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:^BOOL(NSURL *url, NSError *error) {
NSAssert(NO, #"An error has occured");
return YES;
}];
for(NSURL *URL in en) {
NSNumber *documentID = nil;
NSError *error = nil;
BOOL result = [URL getResourceValue:&documentID forKey:NSURLDocumentIdentifierKey error:&error]; \
NSAssert(result == YES && error==nil, #"Unable to read property. Error: %#", error); \
NSLog(#"Processing file: %#", URL);
// This will break most of the times
NSAssert(documentID != nil, #"Document ID should not be nil!!");
}
Perhaps I misunderstood the documentation but it seems to me NSURLDocumentIdentifierKey should be available on every file on disk.
I filed a bug with Apple on this issue and got a feedback on my report. As of today, the information on tracking the DocumentIdentifier is not part of the documentation yet, but the ticket is still open.
The missing information is, that the filesystem does not track the DocumentIdentifier by default. You'll have to enable the tracking by setting a flag on each file that you want to track using chflags with the UF_TRACKED flag.
The following script will print the DocumentIdentifier for a file:
https://gist.github.com/cmittendorf/fac92272a941a9cc64d5
And this script will enable tracking the DocumentIdentifier:
https://gist.github.com/cmittendorf/b680d1a03aefa08583d7
Apparently Yosemite assigns a DocumentIdentifier to a file only when it knows something is trying to track its identity (like Versions or iCloud).
I don't see any way to talk to the kernel and tell it to start tracking files you're interested on. I hope this changes in future releases, since the API has been made public on OS X 10.10 and it's mostly useless at this point.
This issue still exists in macOS 10.14. It probably won't change.
The work-around is to get the inode from NSFileManager, like this:
NSFileManager *fmgr = [NSFileManager defaultManager];
NSDictionary *attributes = [fmgr attributesOfItemAtPath:url.path error:nil;
if (attributes != nil) {
NSNumber *inode = [attributes objectForKey:NSFileSystemFileNumber];
...
}

iOS7 xCode5 how to resolve "invalid product ID" for in app purchases in 2014?

I'm trying to implement in-app purchases in one of my apps, and have an issue where I get no products returned when I send a product request for sandbox testing. I see a lot of very old posts, like this one (invalid product id from 2010). What am I doing wrong with my in app purchase setup? Is there any recent tutorials on how to configure xCode5 to use in app purchasing?
- (void)requestProUpgradeProductData
{
NSSet *productIdentifiers = [NSSet setWithObject:self.productID ];
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
// we will release the request object in the delegate callback
}
Here's the callback for the product request:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *products = response.products;
for(id object in products)
{
//handle valid products
}
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
NSLog(#"Invalid product id: %#" , invalidProductId);
}
[hud hide:YES];
}
Using this option created a duplicate app id with the same number, but different name on hte developer portal. I cannot delete that app ID.
This is the first consumable in app purchase, no need to host content. The app is in "ready to upload binary stage". It has been over 24 hours since I created the in app purchase via itunesconnect
Found a solution here:
http://www.raywenderlich.com/21081/introduction-to-in-app-purchases-in-ios-6-tutorial
Turns out when I created the product in iTunesConnect, I called it "product", when it should've been "com.mysite.product"

How to specify the app name user was using to post - (via app name) using SDK 3.1

Using the new Facebook SDK 3.1 and iOS 6 there are 2 (actually 3) ways to post.
(Seems the new trend is to have more options to make it more simple??) OMG!!
Here is one:
SLComposeViewController *fbPost = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeFacebook];
[fbPost addURL:[NSURL URLWithString:href]];
[self presentViewController:fbPost animated:YES completion:nil];
And this is another way using native dialogs:
[FBNativeDialogs presentShareDialogModallyFrom:self
initialText: nil
image: nil
url: [NSURL URLWithString:href]
handler:^(FBNativeDialogResult result, NSError *error) {
if (error) {
}
else
{
switch (result) {
case FBNativeDialogResultSucceeded:
break;
case FBNativeDialogResultCancelled:
break;
case FBNativeDialogResultError:
break;
}
}
}];
We, developers, think this is cool because we give a nice functionality to the user and also because our app name appears in the post and that can make some promotion of the app.
The funny thing is that latest implementations are not allowing to specify the app name was posting, the name appears after 'via'.
I tried aswell using SLRequest:
ACAccountStore *store = [[ACAccountStore alloc] init];
ACAccountType *fbType = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
NSMutableDictionary *options = [[NSMutableDictionary alloc] init];
(options)[#"ACFacebookAppIdKey"] = kFacebookAppID;
(options)[#"ACFacebookPermissionsKey"] = #[#"publish_stream"];
(options)[#"ACFacebookAudienceKey"] = ACFacebookAudienceFriends;
[store requestAccessToAccountsWithType:fbType options:options completion:^(BOOL granted, NSError *error) {
if(granted) {
// Get the list of Twitter accounts.
NSArray *fbAccounts = [store accountsWithAccountType:fbType];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
(params)[#"link"] = href;
// (params)[#"picture"] = picture;
// (params)[#"name"] = name;
(params)[#"actions"] = #"{\"name\": \"Go Gabi\", \"link\": \"http://www.gogogabi.com\"}";
//Set twitter API call
SLRequest *postRequest = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodPOST
URL:[NSURL URLWithString:#"https://www.facebook.com/dialog/feed"] parameters:params];
//Set account
[postRequest setAccount: [fbAccounts lastObject]];
[postRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if(error)
{
NSLog(#"%#", error.description);
}
else
{
NSLog(#"%#", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);
}
}];
} else {
}
}];
Unfortunatelly to share that name is not so trivial anymore, I wonder why and who was designing the new implementation...
I would appreciate to get some help on that, thanks in advance.
I try to make my questions funny because is soo boring spend time in so trivial topics...
When you use the SLComposeViewController, it's actually the system presenting to you their controller, and it's the user who sends using the post button. Therefore on Facebook it appears as "via iOS".
There's no way to change that.
Using the Facebook SDK 3.1, under the hood it is also using the iOS 6 native integration, so when you're calling the FBNativeDialogs, on iOS 6, it's using SLComposeViewController.
Facebook continued to develop their SDK because they provide a couple of nice modules to use "out of the box" - this includes friends list selector etc... But I believe the biggest reason for Facebook to continue supporting their SDK it for backward compatibility. Under the hood if you're not on iOS 6, it falls back to it's library, and if you are on iOS 6, it uses the system integration.
Facebook is a big thing, and now it's natively available a lot of developers will be using it, just like Twitter's integration last year. The problem of course is at that point the developer has the option to drop older iOS support, or... have a lot of duplicate code, in the sense that they will check for SLComposeViewController and if it's not available (iOS 5) then use the old Facebook SDK... You can imagine how this would become very messy very quickly.
So, the Facebook SDK (3.1) is using iOS system Facebook integration if available, or if not, it's own. In a nutshell, unless you really want the Facebook SDK goodies (friend picket to name one), and you're not planning on supporting iOS < 6 then you don't need to worry about their SDK, just use the Social framework.
So, back to your question, there are 3 ways to post to Facebook ? Actually taking into consideration what I mentioned, there are 2 ways in iOS 6: SLComposeViewController or, SLRequest. On older iOS versions, only 1: Facebook SDK.
Since the SLComposeViewController is owned by the system, not your app, it will always share as "via iOS".
On the other hand SLRequest will show your apps name. When you specify an account for your SLRequest, that account was acquired via the ACAccountStore as a result of passing in some options including ACFacebookAppIdKey, which will be used to determine your Facebook apps name to post onto the users feed as part of the post.
Hope this helps.

Delegate for ios app upgrade

Is there any delegate method that will be called when the user upgrades to or reinstalls a newer version of the iOS app?
I use Core Data to cache some information from server. When the schema of any entity is changed, I need to manually delete the SQLite database from the simulator, otherwise the app will crash on startup, with an error "The model used to open the store is incompatible with the one used to create the store." If there is any delegate method for app upgrade, the deletion could be automated.
You need to use CoreData versioning:
http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CoreDataVersioning/Articles/Introduction.html
Daniel Smith's answer is the proper one, but I just want to add how my app determines its been updated. I look keep a 'current version' string in the defaults. When the app starts up, I compare it to the current version:
defaults has no string - this is the first run of the app
defaults version is different - the user updated the app
defaults is the same - user just restarted the app
Sometimes its nice to know the above. Make sure to save the defaults immediately after you set the tag and do whatever versioning you want, so a crash doesn't have you do it again.
EDIT: how not to crash if he model changes. I use this now, keep the old repository, and tweaking the model, on every tweak it just removes the old one (if it cannot open it) and creates a new one. This is modeled on Apple's code but not sure about what changes I made. In any case you don't get a crash if the model changes.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
//LTLog(#"_persistentStoreCoordinator = %#", _persistentStoreCoordinator);
if (_persistentStoreCoordinator)
{
return _persistentStoreCoordinator;
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *path = [[appDelegate applicationAppSupportDirectory] stringByAppendingPathComponent:[_dbName stringByAppendingPathExtension:#"SQLite"]];
storeURL = [NSURL fileURLWithPath:path];
BOOL fileExists = [manager fileExistsAtPath:path];
if(!fileExists) {
_didCreateNewRepository = YES;
}
if(_createNewRepository) {
[manager removeItemAtURL:storeURL error:nil];
if(fileExists) _didDestroyOldRepository = YES;
_didCreateNewRepository = YES;
}
while(YES) {
__autoreleasing NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
break;
} else {
_persistentStoreCoordinator = nil;
[manager removeItemAtURL:storeURL error:&error];
if(fileExists) {
_didDestroyOldRepository = YES; // caller didn't want a new one but got a new one anyway (old one corrupt???)
_didCreateNewRepository = YES;
}
#ifndef NDEBUG
LTLog(#"CORE DATA failed to open store %#: error=%#", _dbName, error);
#endif
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
Typical reasons for an error here include:
* The persistent store is not accessible
* The schema for the persistent store is incompatible with current managed object model
Check the error message to determine what the actual problem was.
*/
//LTLog(#"Unresolved error %#, %#", error, [error userInfo]);
//abort();
}
}
return _persistentStoreCoordinator;
}
Follow the blog its good:
http://blog.10to1.be/cocoa/2011/11/28/core-data-versioning/