ios8 TouchID detection if fingerprint was added - ios

Im digging into Apple's Touch ID, more precisely the Local Authenticator.
The documentation as of now is pretty sparse.
Its mainly just this:
LAContext *myContext = [[LAContext alloc] init];
NSError *authError = nil;
NSString *myLocalizedReasonString = <#String explaining why app needs authentication#>;
if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
[myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:myLocalizedReasonString
reply:^(BOOL success, NSError *error) {
if (success) {
// User authenticated successfully, take appropriate action
} else {
// User did not authenticate successfully, look at error and take appropriate action
}
}];
} else {
// Could not evaluate policy; look at authError and present an appropriate message to user
}
as taken from https://developer.apple.com/documentation/localauthentication
The idea of using your fingerprint for authentication is nice. But I can add fingerprints in the device if I know the passcode. And its very easy to get the passcode, like you sit in the train next to ur victim and watch him/her enter the passcode.
I want to use the fingerprint as a way of secure authentication but want to be able to detect if new fingerprints were added since the last time I requested the fingerprint.
Apple is doing this for the AppStore. If you want to authenticate a transaction in the AppStore and have added a new Fingerprint since your last transaction, the AppStore requests your AppleId-Password. This is sane behaviour, because the phone might have been taken by someone else who knows the passcode and added his own fingerprint to buy something expensive.
My Question: Can I detect if a new fingerprint was added since the last time that I used Local Authenticator?

This is now possible in iOS9. The property evaluatedPolicyDomainState has been added to LAContext.
If the fingerprint database is modified (fingers were added or removed), the data returned by evaluatedPolicyDomainState will change. The nature of the changes cannot be determined but by comparing data of evaluatedPolicyDomainState after different evaluatePolicy calls you can detect that the set of fingerprints has been modified.
Note that this property is set only when evaluatePolicy is called and a succesful Touch ID authentication was performed, or when canEvaluatePolicy succeeds for a biometric policy.

As Keith stated, in iOS 9 it's possible. You should do it like this.
let context = LAContext()
context.canEvaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, error: nil)
if let domainState = context.evaluatedPolicyDomainState
where domainState == oldDomainState {
// Enrollment state the same
} else {
// Enrollment state changed
}
Every time you add or delete a fingerprint, the domain state changes. You need to call canEvaluatePolicy for evaluatedPolicyDomainStateto be updated.

In short; no.
In a bit more detail; the LocalAuthentication framework is a tightly-guarded black box. The information you get back from it is very limited. Your interaction with it goes something like this:
Ask it if it's able to authenticate for some type of policy (there is only 1 available at time of writing - Biometrics (Touch ID))
If it can, ask it to actually do it
The system takes over for the actual authentication
It lets you know if the authentication was successful or not (if not, it tells you why)
You have no concept of the actual authentication process (which finger was used, for example). This, of course, is by design. Apple does not want, nor need, to give you access to such information.

I would recommend to store the evaluatedPolicyDomainState value into keychain instead of storing it in NSUserDefault.
You can convert the data value of evaluatedPolicyDomainState into string, which is a 44 character string. Below is the code to convert the evaluatedPolicyDomainState data value into string -
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
if let domainState = context.evaluatedPolicyDomainState {
let bData = domainState.base64EncodedData()
if let decodedString = String(data: bData, encoding: .utf8) {
print("Decoded Value: \(decodedString)")
}
}
}
Now if the device owner made any change in Touch ID like adding a new finger Id; then this data value will be changed and you can take necessary steps to handle the change based on your project needs.

This is the solution to verify if a fingerprint was added or removed, and the difference between the Swift and ObjC solution is that canEvaluatePolicy just verify if something changes, while evaluatePolicy opens the modal verification.
Swift 5.2
let context = LAContext()
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
let defaults = UserDefaults.standard
let oldDomainState = defaults.object(forKey: "domainTouchID") as? Data
if let domainState = context.evaluatedPolicyDomainState, domainState == oldDomainState {
// Enrollment state the same
print("nothing change")
} else {
// Enrollment state changed
print("domain state was changed")
}
// save the domain state for the next time
defaults.set(context.evaluatedPolicyDomainState, forKey: "domainTouchID")
Objective-C
- (void)evaluatedPolicyDomainState {
LAContext *context = [[LAContext alloc] init];
__block NSString *message;
// show the authentication UI with reason string
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:#"Unlock access to locked feature" reply:^(BOOL success, NSError *authenticationError) {
if (success) {
// load the last domain state from touch id
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *oldDomainState = [defaults objectForKey:#"domainTouchID"];
NSData *domainState = [context evaluatedPolicyDomainState];
// check for domain state changes
if ([oldDomainState isEqual:domainState]) {
message = #"nothing change";
} else {
message = #"domain state was changed";
}
// save the domain state that will be loaded next time
oldDomainState = [context evaluatedPolicyDomainState];
[defaults setObject:oldDomainState forKey:#"domainTouchID"];
[defaults synchronize];
} else {
message = [NSString stringWithFormat:#"evaluatePolicy: %#", authenticationError.localizedDescription];
}
[self printMessage:message inTextView:self.textView];
}];
}

I would like to add something,
-(BOOL)hasFingerPrintChanged
{
BOOL changed = NO;
LAContext *context = [[LAContext alloc] init];
[context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:nil];
NSData *domainState = [context evaluatedPolicyDomainState];
// load the last domain state from touch id
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *oldDomainState = [defaults objectForKey:#"domainTouchID"];
if (oldDomainState)
{
// check for domain state changes
if ([oldDomainState isEqual:domainState])
{
NSLog(#"nothing changed.");
}
else
{
changed = YES;
NSLog(#"domain state was changed!");
NSString *message = #"Your Touch ID is invalidated, because you have added or removed finger(s).";
}
}
// save the domain state that will be loaded next time
[defaults setObject:domainState forKey:#"domainTouchID"];
[defaults synchronize];
return changed;
}
It's better to store user password etc in keychain.
I'm using https://github.com/reidmain/FDKeychain

Related

Is it possible to get the value of USE TOUCHID FOR iPhone unlock in default settings

Is it possible to Access Default settings > TouchID & Passcode > iPhone Unlock toggle value.
As far as I know, it's not possible.
NO. there is no way to know if user has opted for using TouchID for Unlocking phone.
There is method canEvaluatePolicy: error:
But this tells you if TouchId is configured / Enabled or Not Configured/Not Enabled. If you want to check for Availability of touch Id for your app, you can use canEvaluatePolicy: error:
-(void)canEvaluatePolicy {
LAContext *context = [[LAContext alloc] init];
__block NSString *message;
NSError *error;
BOOL success;
// test if we can evaluate the policy, this test will tell us if Touch ID is available and enrolled
success = [context canEvaluatePolicy: <BR>LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
if (success) {
message = [NSString stringWithFormat:#"Touch ID is available"];
}
else {
message = [NSString stringWithFormat:#"Touch ID is not available"];
}
[super printMessage:message inTextView:self.textView];
}
you can find fully working code from developer.apple.com website:
https://developer.apple.com/library/content/samplecode/KeychainTouchID/Listings/KeychainTouchID_AAPLLocalAuthenticationTestsViewController_m.html
I don't know why you would want to know that however, you can always check if the device supports TouchID and if it has been setup by the user. You do this by creating an LAContext (Local Authentication Context) and calling the function canEvaluatePolicy:error:. That is all I think you can find out about the TouchID settings on a given iPhone through an app. I hope this helps a little :)

Facebook native login is not opening facebook app even if it's installed in device?

i have followed every step described in the docs of facebook-iso-sdk 4.8.0 for iOS 9, but still couldn't preform app switch on "login-with-facebook" in my app, even if facebook app is already installed.
As you can see in screen shot below i have modified info.plist, but still can't get native app switch to work.
I have also double checked for typo-mistakes in info.plist value. and i can assure you they are correct.
Here is my code :-
if (![AppDelegate SharedInstance].login) {
[AppDelegate SharedInstance].login = [[FBSDKLoginManager alloc] init];
}
[AppDelegate SharedInstance].login.loginBehavior = FBSDKLoginBehaviorNative;
[[AppDelegate SharedInstance].login logInWithReadPermissions:#[#"public_profile",#"email",#"user_friends"] fromViewController:self handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
if (error) {
}
else if (result.isCancelled)
{
// Handle cancellations
}
else
{
NSLog(#"result.grantedPermissions == %#",result.grantedPermissions);
if (result.token)
{
[[[FBSDKGraphRequest alloc] initWithGraphPath:#"me" parameters:#{#"fields": #"id, name, email, first_name, last_name"}]
startWithCompletionHandler:^(FBSDKGraphRequestConnection *connection, id result, NSError *error) {
if (!error) {
NSLog(#"fetched user:%#", result);
NSString *userImageURL = [NSString stringWithFormat:#"https://graph.facebook.com/%#/picture?type=large", [result objectForKey:#"id"]];
[dictFacebookDetail addEntriesFromDictionary:result];
[dictFacebookDetail setObject:userImageURL forKey:#"profilepic"];
NSLog(#"facebook login result --- %#",dictFacebookDetail);
[self performSelectorInBackground:#selector(CheckFacebookUser:) withObject:dictFacebookDetail];
}
}];
}
}
}];
What am i missing ?
I have found a solution, but you should change something in FBSDKLogin pod. I was debugging the Pod and I realized that Facebook ask in the class FBSDKServerConfiguration for the server configuration for the app. It returns a JSON with some information to configure the Pod for our app. I realized that by default the JSON returns this dictionary:
"ios_sdk_dialog_flows" = {
default = {
"use_native_flow" = 0;
"use_safari_vc" = 1;
};
message = {
"use_native_flow" = 1;
};
};
By default the use_native_flow is 0, so when it saves the information in userDefaults for the next app launches.
So, when the app calls FBSDKLoginMananger login method and checks for the loginBehaviour in this method, the variable useNativeDialog returns NO. So the switch uses the next case. case FBSDKLoginBehaviorBrowser:
- (void)logInWithBehavior:(FBSDKLoginBehavior)loginBehavior
{
.
.
...
switch (loginBehavior) {
case FBSDKLoginBehaviorNative: {
if ([FBSDKInternalUtility isFacebookAppInstalled]) {
[FBSDKServerConfigurationManager loadServerConfigurationWithCompletionBlock:^(FBSDKServerConfiguration *serverConfiguration, NSError *loadError) {
BOOL useNativeDialog = [serverConfiguration useNativeDialogForDialogName:FBSDKDialogConfigurationNameLogin];
if (useNativeDialog && loadError == nil) {
[self performNativeLogInWithParameters:loginParams handler:^(BOOL openedURL, NSError *openedURLError) {
if (openedURLError) {
[FBSDKLogger singleShotLogEntry:FBSDKLoggingBehaviorDeveloperErrors
formatString:#"FBSDKLoginBehaviorNative failed : %#\nTrying FBSDKLoginBehaviorBrowser", openedURLError];
}
if (openedURL) {
completion(YES, FBSDKLoginManagerLoggerAuthMethod_Native, openedURLError);
} else {
[self logInWithBehavior:FBSDKLoginBehaviorBrowser];
}
}];
} else {
[self logInWithBehavior:FBSDKLoginBehaviorBrowser];
}
}];
break;
}
// intentional fall through.
}
case FBSDKLoginBehaviorBrowser: {
.
.
.
}
As we see in the code, we know if the app is installed in this if,
if ([FBSDKInternalUtility isFacebookAppInstalled]).
To solve the problem, I have changed this line
BOOL useNativeDialog = [serverConfiguration useNativeDialogForDialogName:FBSDKDialogConfigurationNameLogin];
to
BOOL useNativeDialog = YES;
I know this is not a good practice and it will change if I update this Pod, but at least is working and I needed it now.
I guess we can change that configuration in facebook developers admin site, but I haven't found anything.
Facebook has changed Facebook login behavior for iOS9.
Here is the quote from Facebook blog post:
We've been monitoring data and CTRs for over 250 apps over the last 6 weeks since iOS 9 launched. The click-through rate (CTR) of SVC Login outperforms the CTR of app-switch Login and is improving at 3x the rate of the app-switch experience. This indicates that the SVC experience is better for people and developers today, and will likely be the best solution in the long run. For this reason, the latest Facebook SDK for iOS uses SVC as the default experience for Login.
typedef NS_ENUM(NSUInteger, FBSDKLoginBehavior)
{
/*!
#abstract This is the default behavior, and indicates logging in through the native
Facebook app may be used. The SDK may still use Safari instead.
*/
FBSDKLoginBehaviorNative = 0,
/*!
#abstract Attempts log in through the Safari or SFSafariViewController, if available.
*/
FBSDKLoginBehaviorBrowser,
/*!
#abstract Attempts log in through the Facebook account currently signed in through
the device Settings.
#note If the account is not available to the app (either not configured by user or
as determined by the SDK) this behavior falls back to \c FBSDKLoginBehaviorNative.
*/
FBSDKLoginBehaviorSystemAccount,
/*!
#abstract Attemps log in through a modal \c UIWebView pop up
#note This behavior is only available to certain types of apps. Please check the Facebook
Platform Policy to verify your app meets the restrictions.
*/
FBSDKLoginBehaviorWeb,
};
there is lot of behaviour available to access the fb login.try using alternate what you prefer from this.
FBSDKLoginManager *loginmanager = [[FBSDKLoginManager alloc] init];
loginmanager.loginBehavior=FBSDKLoginBehaviorNative;
Like this ...hope this will help you :)
(FBSDKServerConfiguration *)_defaultServerConfigurationForAppID:(NSString *)appID
{
// Use a default configuration while we do not have a configuration back from the server. This allows us to set
// the default values for any of the dialog sets or anything else in a centralized location while we are waiting for
// the server to respond.
static FBSDKServerConfiguration *_defaultServerConfiguration = nil;
if (![_defaultServerConfiguration.appID isEqualToString:appID]) {
// Bypass the native dialog flow for iOS 9+, as it produces a series of additional confirmation dialogs that lead to
// extra friction that is not desirable.
NSOperatingSystemVersion iOS9Version = { .majorVersion = 9, .minorVersion = 0, .patchVersion = 0 };

How to display an AlertView requesting permission to seed iCloud once and only once in app's iCloud lifetime?

I have one question near the end.
I am working from the belief/experience that seeding iCloud more than once is a bad idea and that if a user can do the wrong thing, he probably will sooner or later.
What I want to do:
A. When the user changes the app preference "Enable iCloud" from NO to YES, display AlertView asking (Yes or No) if the user wishes to seed the cloud with existing non-iCloud Data.
B. Ensure that the app seeds iCloud only once on an iCloud account, refraining to put up the AlertView once seeding is completed the first time.
My Method:
Following Apple's Docs concerning the proper use of NSUbiquitousKeyValueStore, I am using the following method in, - (void)application: dFLWOptions:
- (void)updateKVStoreItems:(NSNotification*)notification {
// Get the list of keys that changed.
NSDictionary* userInfo = [notification userInfo];
NSNumber* reasonForChange = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
NSInteger reason = -1;
// If a reason could not be determined, do not update anything.
if (!reasonForChange)
return;
// Update only for changes from the server.
reason = [reasonForChange integerValue];
if ((reason == NSUbiquitousKeyValueStoreServerChange) ||
(reason == NSUbiquitousKeyValueStoreInitialSyncChange)) { // 0 || 1
// If something is changing externally, get the changes
// and update the corresponding keys locally.
NSArray* changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
// This loop assumes you are using the same key names in both
// the user defaults database and the iCloud key-value store
for (NSString* key in changedKeys) {//Only one key: #"iCloudSeeded" a BOOL
BOOL bValue = [store boolForKey:key];
id value = [store objectForKey:#"iCloudSeeded"];
[userDefaults setObject:value forKey:key];
}
}
}
Include the following code near the top of application: dFLWO:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(updateKVStoreItems:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:store]; // add appDelegate as observer
After loading iCloud Store, then seed it with non-iCloud data ONLY if seeding has never been done
- (BOOL)loadiCloudStore {
if (_iCloudStore) {return YES;} // Don’t load iCloud store if it’s already loaded
NSDictionary *options =
#{
NSMigratePersistentStoresAutomaticallyOption:#YES
,NSInferMappingModelAutomaticallyOption:#YES
,NSPersistentStoreUbiquitousContentNameKey:#"MainStore"
};
NSError *error=nil;
_iCloudStore = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil URL:[self iCloudStoreURL] options:options error:&error];
if (_iCloudStore) {
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded =
[store boolForKey:#"iCloudSeeded"];//If the key was not found, this method returns NO.
if(!iCloudSeeded) // CONTROL IS HERE
[self confirmMergeWithiCloud]; // Accept one USER confirmation for seeding in AlertView ONCE world wide
return YES; // iCloud store loaded.
}
NSLog(#"** FAILED to configure the iCloud Store : %# **", error);
return NO;
}
Once the seeding is completed do the following to prevent any repeat seeding:
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloud];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"]; // NEVER AGAIN
//[store synchronize];
}
}
}
Be sure to get a total iCloud reset before the above process using:
[NSPersistentStoreCoordinator
removeUbiquitousContentAndPersistentStoreAtURL:[_iCloudStore URL]
options:options
error:&error])
This is a very tidy solution to my problem, IMHO, but I can not quite get it done.
MY QUESTION:
How do I respond to the first notification to updateKVStoreItems: above? It is a notification with bad info. I says the value is TRUE, but I have never set it to TRUE. How do I set default values for a key in NSUbiquitousKeyValueStore?
I find that the first notification is of reason : NSUbiquitousKeyValueStoreInitialSyncChange
When that note comes in, bValue is YES. THIS IS MY PROBLEM. It is as if, iCloud/iOS assumes any new BOOL to be TRUE.
I need this value to be NO initially so that I can go ahead and follow the Apple Docs and set
the NSUserDefault to NO. And then Later when the seeding is done, to finally set the value: YES for the key:#"iCloudSeeded"
I find I can not penetrate the meaning of the following from Apple:
NSUbiquitousKeyValueStoreInitialSyncChange
Your attempt to write to key-value storage was discarded because an initial download from iCloud has not yet happened.
That is, before you can first write key-value data, the system must ensure that your app’s local, on-disk cache matches the truth in iCloud.
Initial downloads happen the first time a device is connected to an iCloud account, and when a user switches their primary iCloud account.
I don't quite understand the implications of number 2 below, which I found online:
NSUbiquitousKeyValueStoreInitialSyncChange – slightly more complicated, only happens under these circumstances:
1. You start the app and call synchronize
2. Before iOS has chance to pull down the latest values from iCloud you make some changes.
3. iOS gets the changes from iCloud.
If this problem was with NSUserDefaults and not NSUbiquitousKeyValueStore, I believe I would need to go to registerDefaults.
I am almost there,
How do I do this please!
Thanks for reading, Mark
The code was looking for both
A. NSUbiquitousKeyValueStoreInitialSyncChange and
B. NSUbiquitousKeyValueStoreServerChange
I was unable to figure out what to do with the notifications. I know see that I did not need to do anything with either. My app only needs to read and write, in order to solve the problem I laid out in my question header.
The app gets the current value with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded = [store boolForKey:#"iCloudSeeded"];
The app sets the value in the NSUbiquitousKeyValueStore with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"];
I believe I am correct in saying the following: Writing is done into memory. Very soon thereafter the data is put by the system onto disk.
From there it is taken and put into iCloud and is made available to the other devices running the same app on the same iCloud account. In the application I have described, no observer needs to be added, and
nothing else needs to be done. This is maybe an "unusual" use of NSUbiquitousKeyValueStore.
If you came here looking for a an more "usual" use, say when a user type something into a textview and it later
appears on a view of other devices running the same app, check out a simple demo I came across at :
https://github.com/cgreening/CMGCloudSyncTest
The better functioning (monitoring only) notification handler follows:
- (void)updateKVStoreItems:(NSNotification*)notification {
NSNumber *reason = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
if(!reason) return;
// get the reason code
NSInteger reasonCode = [notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] intValue];
BOOL bValue;
NSUbiquitousKeyValueStore *store;
switch(reasonCode) {
case NSUbiquitousKeyValueStoreServerChange:{ // code 0, monitoring only
store = [NSUbiquitousKeyValueStore defaultStore];
bValue = [store boolForKey:#"iCloudSeeded"];
id value = [store objectForKey:#"iCloudSeeded"];
DLog(#"New value for iCloudSeeded=%d\nNo Action need be take.",bValue);
// For monitoring set in UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:value forKey:#"iCloudSeeded"];
break;
}
case NSUbiquitousKeyValueStoreAccountChange: {// ignore, log
NSLog(#"NSUbiquitousKeyValueStoreAccountChange");
break;
}
case NSUbiquitousKeyValueStoreInitialSyncChange:{ // ignore, log
NSLog(#"NSUbiquitousKeyValueStoreInitialSyncChange");
break;
}
case NSUbiquitousKeyValueStoreQuotaViolationChange:{ // ignore, log
NSLog(#"Run out of space!");
break;
}
}
}
Adding 9/3/14
So sorry but I continued to have trouble using a BOOL, I switched to an NSString and now
all is well.
METHOD TO ENSURE THAT THE "MERGE" BUTTON FOR SEEDING ICOUD IS USED AT MOST ONCE DURING APP LIFETIME
Use NSString and not BOOL in KV_STORE. No need to add observer, except for learning
In Constants.h :
#define SEEDED_ICLOUD_MSG #"Have Seeded iCloud"
#define ICLOUD_SEEDED_KEY #"iCloudSeeded"
Before calling function to seed iCloud with non-iCloud data:
NSUbiquitousKeyValueStore* kvStore = [NSUbiquitousKeyValueStore defaultStore];
NSString* strMergeDataWithiCloudDone =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
NSComparisonResult *result = [strMergeDataWithiCloudDone compare:SEEDED_ICLOUD_MSG];
if(result != NSOrderedSame)
//put up UIAlert asking user if seeding is desired.
If user chooses YES : set Value for Key after the merge is done.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloudwithNoniCloudData];
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
[store setObject:SEEDED_ICLOUD_MSG forKey:ICLOUD_SEEDED_KEY];
}
}
}
Thereafter on all devices, for all time, the code
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
NSString* msg =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
produces: msg == SEEDED_ICLOUD_MESSAGE

How to handle failure while delivering content after In App Purchase?

I have an iOS app that uses In App Purchase.
It uses a consumable type product which when purchased by a user allows him/her to upload an audio to the server.
Once the InAppPurchase is successful, the data is uploaded to the server.
How do we handle the case when the InAppPurchase is successful but the uploading fails?
We shouldn't make the user buy the product again so I'm storing whether the upload is successful or not in user defaults.
Here is the code
SKProduct *productToBuy = //product to buy.
if ([[[NSUserDefaults standardUserDefaults] objectForKey:productToBuy.productIdentifier] boolValue]) //if the user has already bought the product and it failed to upload, then just try to upload without buying..
{
NSLog(#"already purchased, uploading");
[self uploadRecording];
}
else //if not, then make the user purchase and then upload
{
[[CGInAppPurchaseManager sharedManager] buyProduct:productToBuy withCompletionBlock:^(SKPaymentTransaction *paymentTransaction, BOOL success) {
if (success)
{
[self uploadRecording];
}
else
{
if (paymentTransaction.error.code != SKErrorPaymentCancelled)
{
NSLog(#"Cancelled transaction");
}
}
}];
}
The uploadRecording code looks like this
- (void)uploadRecording
{
NSURL *urlOfAudio = //url of audio file
[self.model uploadAdOrDedicationWithTitle:title audioData:[NSData dataWithContentsOfURL:urlOfAudio] withCompletionBlock:^(id obj, NSInteger errCode, NSString *errorDescription) {
SKProduct *productToBuy = self.adParams[#"product"];
if (obj)
{
//if its successfully uploaded set false that the user has already bought a product with this identifier
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:productToBuy.productIdentifier];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else
{
//when there is an error set true that the user has already bought a product with this identifier
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES] forKey:productToBuy.productIdentifier];
[[NSUserDefaults standardUserDefaults] synchronize];
//show appropriate error message
//"An error occured while uploading your broadcast. You will not be made to purchase the product again the next time you try to upload a recording for the same product."
}
}];
}
Is this okay? Or is there a better way to handle this?
Ideally you should refund, but it isn't possible, so you are stuck with trying the minimize and remedy the problem the best way you can. Your solution is a remedy that does seem like the least you can do. Additionally, if there is a way to do some verification to check that the upload is likely to function ahead of the actual upload (using the Reachability API or better still connecting to your server for a quick availability check), then this would minimize the risk of this occurring.

What should the logic for first run, internet available be?

For an app that fetches web from a web service, I have included a plist to be parsed into CoreData if its the first run because the data is not readily available in the Docs directory or may take long to fetch from the web. I do have NSNotifications signaling when a web fetch/synchronization has succeeded though.
At present in AppDelegate applicationDidFinishLaunchingWithOptions I call:
[self checkIfFirstRun];
which is this:
-(void)checkIfFirstRun{
NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
NSString *appFirstStartOfVersionKey = [NSString stringWithFormat:#"first_start_%#", bundleVersion];
NSNumber *alreadyStartedOnVersion = [[NSUserDefaults standardUserDefaults] objectForKey:appFirstStartOfVersionKey];
if(!alreadyStartedOnVersion || [alreadyStartedOnVersion boolValue] == NO) {
// IF FIRST TIME -> Preload plist data
UIAlertView *firstRun = [[UIAlertView alloc] initWithTitle:#"1st RUN USE LOCAL DB"
message:#"FIRST"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
[firstRun show];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:[NSNumber numberWithBool:YES] forKey:appFirstStartOfVersionKey];
[prefs synchronize];
//Use plist
[self parsePlistIntoCD];
} else {
UIAlertView *secondRun = [[UIAlertView alloc] initWithTitle:#"nTH RUN WEB FETCH"
message:#"nTH"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
[secondRun show];
}
}
So ok, i get my plist parsed perfectly into my CoreData db.
Here is the parsePlistIntoCD:
-(void)parsePlistIntoCD{
self.managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// 3: Now put the plistDictionary into CD...create get ManagedObjectContext
NSManagedObjectContext *context = self.managedObjectContext;
NSError *error;
//Create Request & set Entity for request
NSFetchRequest *holidayRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *topicEntityDescription = [NSEntityDescription entityForName:#"Holiday" inManagedObjectContext:context];
[holidayRequest setEntity:topicEntityDescription];
//Create new NSManagedObject
//Holiday *holidayObjectToSeed = nil;
Holiday *newHoliday = nil;
//Execute fetch just to make sure?
NSArray *holidayFetchedArray = [context executeFetchRequest:holidayRequest error:&error];
if (error) NSLog(#"Error encountered in executing topic fetch request: %#", error);
// No holidays in database so we proceed to populate the database
if ([holidayFetchedArray count] == 0) {
//Get path to plist file
NSString *holidaysPath = [[NSBundle mainBundle] pathForResource:#"PreloadedFarsiman" ofType:#"plist"];
//Put data into an array (with dictionaries in it)
NSArray *holidayDataArray = [[NSArray alloc] initWithContentsOfFile:holidaysPath];
NSLog(#"holidayDataArray is %#", holidayDataArray);
//Get number of items in that array
int numberOfTopics = [holidayDataArray count];
//Loop thru array items...
for (int i = 0; i<numberOfTopics; i++) {
//get each dict at each node
NSDictionary *holidayDataDictionary = [holidayDataArray objectAtIndex:i];
//Insert new object
newHoliday = [NSEntityDescription insertNewObjectForEntityForName:#"Holiday" inManagedObjectContext:context];
//Parse all keys in each dict object
[newHoliday setValuesForKeysWithDictionary:holidayDataDictionary];
//Save and or log error
[context save:&error];
if (error) NSLog(#"Error encountered in saving topic entity, %d, %#, Hint: check that the structure of the pList matches Core Data: %#",i, newHoliday, error);
};
}
[[SDSyncEngine sharedEngine] startSync];
}
The thing is, I need to also make sure that if there is internet available, that my CoreData db get repopulated with the fetched web data.
But If I leave the call to [self parsePlistIntoCD]; only the plist data is present in the CoreData. First or nth run, I only get the plist data. If I comment that line out, I get my web fetched data.
Why doesnt the web fetched data replace the plist parsed data?
So the logic of parsePlistIntoCD is essentially
if no objects in store, load them from plist
always invoke startSync on [SDSyncEngine sharedEngine], which handles the web download and sync.
It looks to me like your startSync will in fact be invoked. So I would look there for the bug. You could add a log statement, or set breakpoints, to verify that that code path is actually being followed.
Both the plist parse and the web data fetch might take some time. That's a sign that you should be doing these operations in the background, perhaps with a GCD queue. You don't know in advance whether either of them will succeed. So don't set the preferences until they finish.
Side note: you can query the preferences database for BOOLs, making your code shorter, and therefore easier to read.
BOOL alreadyStartedOnVersion = [[NSUserDefaults standardUserDefaults] boolForKey:appFirstStartOfVersionKey];
and
[prefs setBool:YES forKey:appFirstStartOfVersionKey];
You can also replace numberWithBool: with simply #(YES) and #(NO).
For your program logic, I suggest something like this:
In -applicationDidFinishLaunchingWithOptions:, check to see if the starting plist data has been loaded. Forget about whether it's the first run. Just see whether the plist data needs to be loaded. Maybe call that shouldLoadPlistData. Or maybe you need to tie that to the version you're running, in which case you'd store a string latestPlistVersionLoaded.
If you haven't loaded it yet, enqueue a block to perform the plist load. At the conclusion of the plist load, set shouldLoadPlistData to NO, to note that plist data no longer needs to be loaded. If, for some reason, the plist load fails (maybe the phone runs out of battery or your app is killed by user or system), then on the next launch you're back where you started.
also check to see whether you have net access. If you do, enqueue a block to retrieve the web-based data, parse the data, and then, upon conclusion, update the preferences.
If the data is large, you might want to checkpoint this work:
Do I have the full web update? Then I'm done. Otherwise...
Has the download finished? Yay, I have the data, let's load it.
If not, have I started the download?
This staged checkpointing will also allow you to ask the system for extra time, if your app exits in the middle of the download.
parseListIntoCD feels a bit bloated to me. It does more than its name implies. Perhaps you could refactor it into a check (shouldLoadPlist), a method that does the import (importPlist:intoContext:), and a method that fires off the sync.
I strongly suggest that you pass the working NSManagedObjectContext in as a parameter, rather than having some global object that dispenses MOCs (as [SDCoreDataController sharedInstance] appears to do. It gives you much more control, and allows you to write unit tests much more easily. If you also pass in the path to the plist, you now have clean code that should behave the same way every time you call it.
Your use of the NSError ** parameter is consistently incorrect. The value of NSError is undefined upon success. You must test the result of the operation, not the value of the error, to determine whether you succeeded. The idiom is always
if (![someObject doTaskWithObject:foo error:&error]) {
// handle the error
}
Take a look also at countForFetchRequest:error. It would give you the same info that you're currently extracting by performing a fetch and counting results, but without having to instantiate the NSManagedObjects.

Resources