Control push notification device registration - ios

I am implementing the push notification service in my app. I created a the service on the Easy Apns style, but in a simpler way. I have a service that uses a MySql database to store the application tokens and a second service that accesses to the database, prepares the notifications and sends them.
It works, but I'm not sure if my registration part (on the client side) is correct.
The app delegate method:
- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
NSString *devToken = [[[[deviceToken description]
stringByReplacingOccurrencesOfString:#"<"withString:#""]
stringByReplacingOccurrencesOfString:#">" withString:#""]
stringByReplacingOccurrencesOfString: #" " withString: #""];
NSString *urlString = [#"/apns.php?"stringByAppendingString:#"task=register"];
urlString = [urlString stringByAppendingString:#"&appname="];
NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleDisplayName"];
urlString = [urlString stringByAppendingString:appName];
urlString = [urlString stringByAppendingString:#"&devicetoken="];
urlString = [urlString stringByAppendingString:devToken];
NSString *host = #"myservice.php";
NSURL *url = [[NSURL alloc] initWithScheme:#"http" host:host path:urlString];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
}
This code updates the token each time the app is restarted. Is it correct? Shouldn't the registration phase happen just once (for example, the first time the user accepts the notification service)? In my case, every time the app is restarted, the token is added to my database (so I have to control from the server side if it is a replicated token).

There's nothing wrong in the way you are registering to APNS, but you can do it more efficiently.
Apple suggests that you register to the APN service every time the app is launched (and not only on the first time it is launched), because theoretically the device token might change (though it practice it almost never does).
An application should register every time it launches and give its provider the current token.
By requesting the device token and passing it to the provider every time your application launches, you help to ensure that the provider has the current token for the device. If a user restores a backup to a device or computer other than the one that the backup was created for (for example, the user migrates data to a new device or computer), he or she must launch the application at least once for it to receive notifications again. If the user restores backup data to a new device or computer, or reinstalls the operating system, the device token changes. Moreover, never cache a device token and give that to your provider; always get the token from the system whenever you need it. If your application has previously registered, calling registerForRemoteNotificationTypes: results in the operating system passing the device token to the delegate immediately without incurring additional overhead.
You can store the device token locally in NSUserDefaults, and each time didRegisterForRemoteNotificationsWithDeviceToken is called, you can compare the newly received device token with the stored device token. You need to send the device token to your server only if it's different than the stored one. That would save unnecessary server calls.

Related

Lost connectivity before receiving purchase receipt from apple In-app purchase

I have implemented In-app purchase in my app. So what I do is, after user completes payment with app store & the control goes to
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
then I update the purchase receipt to our server. Rest of the operations are done by server.
But one of our users faced an issue where he completed the payment but he lost internet connection before the above method was called. Resulting, our app does not know about the payment.
I know that the receipt is stored in the device. But is it the right way to send the receipt to the server from device every time user uses app??
Well, What i did to avoid this was, Keep a Check (like a boolean : transactionInProgress) when the transaction begin and when I have sent data to the server I change the value to "NO".
In this way the next time the app connects. just check if the transactionInProgress was checked, if it is then there was a transaction on going and you can fetch the receipt that must be stored by SKPaymentTransactions using
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt) { /* No local receipt -- handle the error. */ }

Will performFetchWithCompletionHandler be called if the app has been terminated

It's surprisingly difficult to find a definitive answer to this; couldn't find it mentioned in the Apple documentation and couldn't find a definite yes/no after searching past questions.
The question is simple - if the app requests a background fetch to be performed after N time, then the user terminates the app. Will the OS still launch the app into the background to perform the background fetch?
Okay, once again background modes cause confusion. No offense to the other people trying to help, but this is more complicated than it seems.
First of all:
This is out of date, as Sausage guessed in the comments. I know this for a fact, because the section about VoIP apps is still explaining the "old way" to do this, with a handler that gets called periodically. I investigated this a bit for this answer, so I suggest you go and read that. The important lesson for this case here is that iOS makes a distinction between an app being terminated by the user or by the system, plus it also plays a role whether the phone was rebooted or not.
So to sum this (and your question) up you basically want to know whether this part of the above, outdated documentation is still correct word for word:
In most cases, the system does not relaunch apps after they are force quit by the user. One exception is location apps, which in iOS 8 and later are relaunched after being force quit by the user. In other cases, though, the user must launch the app explicitly or reboot the device before the app can be launched automatically into the background by the system. When password protection is enabled on the device, the system does not launch an app in the background before the user first unlocks the device.
Apple: Understanding When Your App Gets Launched into the Background
I thoroughly investigated the rest of the docs, but did not find any definite answer, so it unfortunately boils down to what dan already suggested: Test it. My gut feeling is that the documentation is still correct in that regard, though (as said what's not is the VoIP stuff). I say that because the UI in the Settings app calls the feature "Background App Refresh", so users are probably supposed to understand that an app having this permission won't refresh when they "push" them out of background (i.e. home button -> swipe it out). For regular users, apps are either quit (not in the task manager at all), in the foreground (using them) or in background (they're in the task manager and another app is in foreground and/or the phone is locked).
To really test this you'd have to write an app and actually carry it around a bit (I assume at least two days) in each condition. First while it is in background (the OS should periodically let it fetch, as you probably know this can also be triggered in Xcode) and then while it is force-quit. The problem is to verify that it fetched stuff. I'd go with a logfile that can be shared via iTunes. I have typed up some code for this:
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(#"We're awake! Booyah!");
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest *request = [NSMutableURLRequest new];
request.HTTPMethod = #"GET";
request.URL = [NSURL URLWithString:#"https://www.google.com"];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
NSDate *now = [NSDate date];
NSString *toLog = [NSString stringWithFormat:#"%# - fetched\n",
[now description]];
[self updateTestDocumentWithString:toLog];
NSLog(#"Yay, done!");
completionHandler(UIBackgroundFetchResultNewData);
}];
[task resume];
}
- (void)updateTestDocumentWithString:(NSString *)toAppend {
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [[docDir stringByAppendingPathComponent:#"logfile.txt"] copy];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
if (![[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]) {
NSLog(#"We're effed...");
return;
}
}
NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
if (!file) {
NSLog(#"We're effed again...");
return;
}
[file seekToEndOfFile];
// ensure this is never nil
[file writeData:[toAppend dataUsingEncoding:NSUTF8StringEncoding]];
[file closeFile];
}
This would go into the app delegate, and don't forget to add the Application supports iTunes file sharing boolean setting in your app's plist. I will leave this running on my development device for a bit and check the logfile, eventually reporting back here. Feel free to test it yourself, too.
EDIT:
https://devforums.apple.com/message/873265#873265 (login required)
Also keep in mind that if you kill your app from the app switcher
(i.e. swiping up to kill the app) then the OS will never relaunch the
app regardless of push notification or background fetch. In this case
the user has to manually relaunch the app once and then from that
point forward the background activities will be invoked. -pmarcos
That post was by an Apple employee so I think i can trust that this information is correct.
OLD answer:
According to this answer wrote by a top user: iOS background fetch: your app won't be woken up again.
Make sure you're not killing the app (i.e. by double tapping on the
home button and swiping up on your app for force the app to
terminate). If the app is killed, it will prevent background fetch
from working correctly.
It really doesn't make sense for it to be woken up...it kinda invalidates the user killing the app.
Having that said there are different ways a terminated/force quit app can be launched again:
Tapping on a notification.
Tapping on the app icon.
Using openUrl to open your app from another app.
If you use PushKit...then your app would be launched. Imagine if had a VOIP app e.g. Skype, WhatsApp and a friend was calling you but you had have force-quit the app, you wouldn't receive calls. For more see here.
Location updates either through use of region monitoring or the significant-change location service. See this answer and make sure to read this entire page from Apple docs.
Rebooting the device would also undo anything blocked through force-quit
Reading the Apple documentation here I found this text snippet which should explain your question:
The techniques offered by iOS fall into three categories:
Apps that start a short task in the foreground can ask for time to finish that task when the app moves to the background.
**Apps that initiate downloads in the foreground can hand off management of those downloads to the system, thereby allowing the app to be suspended or terminated while the download continues.**
Apps that need to run in the background to support specific types of tasks can declare their support for one or more background execution modes.
The second option is exactly about downloading the data, which can be delegated to the system even if the can be terminated.

Uniquely identify the device in iOS

I am developing a corporate application on the iPad for a certain business requirement.
This app is meant to use in a specific number of devices which is predefined by the admin.
But I also need the application to reject any login requests even if it is from an authorised user,when he or she is using a device which is not defined by the admin.
Edits:
Say I have 2 devices and I have my credentials to login to the app, And my need is, to restrict the login from the devices which is not mine.
For that I have to identify whether the login request is comes from my device or not.
Previously we could use device UDID to do this, but now it is deprecated.
Can any one please suggest a method to implement this ?
try this. for more info check UIDevice
// IOS 6+
NSString *uniqueIdentifier = [[NSString alloc] initWithString:[[[UIDevice currentDevice] identifierForVendor] UUIDString]];
You can use iCloud over here because UUID has been deprecated and vendorId is uniqe but might be change if you uninstall the app and install it agian,
So I would suggest iCloud will be safer, what you cna do is at the time of application launch you can generate one token which is unique and save it to your iCloud data storage along with user credentials,
So from the next time onwards when user will try to login you can check it with iCloud.
How about using Apple's enterprise distribution system? That will allow you to deploy the app to a corporation, and have tight access control.
https://developer.apple.com/programs/ios/enterprise/
Just Trick:
You can Implement APNS code to your Project and Get Device Token.
The Device Token is Unique One. But The user Must Allow the APNS.
Note: APNS Device Token is changed to Following reasons.
Change of Bundle id.
Change of Development Mode(Sandbox/Production)
I have implemented a solution for exactly your problem, The best solution (and Apples recommended route) is to create a UUID unique to your App like this:
NSString *uuidString = nil;
CFUUIDRef uuid = CFUUIDCreate(NULL);
if (uuid) {
uuidString = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, uuid));
CFRelease(uuid);
}
Then, and this is the key, you can store that to the iOS keychain (Handy classes here: https://github.com/lukef/IXKeychain) and values in the iOS keychain are NOT removed when the user uninstalls the App, so you can persist your own UUID through App installs which is a key part of managing a specific number of devices against a user account.
This method will return a string for every device. Since it is gonna change every time for single device
so we are storing it in a keychain and can refer it whenever we need it.
+ (NSString *) uniqueDeviceIdentifier
{
NSString *deviceUUID = [[SGKeyChain defaultKeyChain] stringForKey:#"uniqueId"];
if (!deviceUUID) {
if (!deviceUUID.length) {
NSString *deviceUUID = #"";
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef);
CFRelease(uuidRef);
deviceUUID = [NSString stringWithFormat:#"%#",[NSString stringWithString:(__bridge_transfer NSString *)uuidStringRef]];
[[SGKeyChain defaultKeyChain] setObject:deviceUUID forKey:#"uniqueId" accessibleAttribute:kSecAttrAccessibleAlways];
}
}
return deviceUUID;
}
yo can refer to this repository... https://github.com/sgup77/SGKeyChainWrapper for SGKeyChain implementation
Ok I will share the approach that we follow for a B2B enterprise app.
.
Every User has login Id and Password.
1. So user register his device with Server using deviceReg API which takes clientDeviceId as param(client generated uuid) along with username and pass.
2. Server returns a server generated unique identifier to be used by application on that particular device.
Conclude - in this way you can restrict the user with a certain device.
You can use below method to generate deviceSpecific client UUID
- (NSString *)getUuid
{
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef);
CFRelease(uuidRef);
NSString *uuid = [NSString stringWithString:(__bridge NSString *)uuidStringRef];
CFRelease(uuidStringRef);
return uuid;
}
Please note:
I remove the explanation about using AuthKey, AccessToken and others which we use for security purpose as you do not use any auth server.
I hope it helps.
Update 1.
Since you are having an enterprise application so i am sure you would be having atleast the user e-mail ids.
So the account manager should than 1st send a email with one time token to all active accounts.
This token can be requested by the application while registering the device and send to server for validation.
Also the server invalidates the token once used to avoid misuge.
There should be a migrAtion api which uses the server generated device id and a migration token if user migrates the device.

Communicating iOS chat service with a web chat service

I'm searching for a cross-platform chat service that supports mobile (Android and iOS), and Desktop. I've looked into a few sites that offer chat services, but none of them seem to be what I'm looking for in terms of features. So I've decided to try to create an iOS application that will connect to a service that we will use for the desktop version.
That was just a bit of background info, but here's the question:
I am creating a url request in order to send information to the server that will handle and store my url request like so:
-(IBAction)clickedSendButton:(id)sender
{
urlReq = [NSString stringWithFormat:#"%#/mobile/PrivateChatMsgSending/?sid=%#&skey=%#&rid=%#&msg=%#",domain,((ApplicationAppDelegate *) [UIApplication sharedApplication].delegate).loggedProfileID,((ApplicationAppDelegate *) [UIApplication sharedApplication].delegate).loggedSessionID,receipientProfileId,txtFldChat.text];
NSURL *url = [NSURL URLWithString:[urlReq stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURLRequest *urlrequest = [NSURLRequest requestWithURL:url];
connection = [[[NSURLConnection alloc] initWithRequest:urlrequest delegate:self] autorelease];
[self ShowIndicatorView:#"Sending..."];
}
So would I be able to pass these parameters to my php script on the server side all while being compatible with my desktop version? Is there anything within my application that could potentially inhibit my user's from receiving messages from desktop users? What might be a better solution to creating a multi-platform messaging service?

APNS token collision, stored in Postgres

I use push notifications and store device tokens like I assume everyone else does. First I transform them into a string my app:
NSString *deviceTokenString = [[[token description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:#"<>"]]
stringByReplacingOccurrencesOfString:#" " withString:#""];
Then I PUT them to my server, where ActiveRecord stores them in a character varying(255) column:
Device.where(:token => device_token, :username => username).first_or_create!(:model => model)
I have a validation that ensures no two tokens are the same, which I understand should always be the case:
class Device < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :token
end
However, I've started to see validation errors for token uniqueness:
ActiveRecord::RecordInvalid: Validation failed: Token has already been taken
Manual query in psql confirms that a device is trying to register with a token already in the table under a different user. Isn't this supposed to be impossible? Is something in the way I'm transforming tokens truncating them? I checked every code example I could find when the problem first occurred and everyone seems to use the method I've listed in the first code sample.
It can happen that a device tries to register with a token already in the table under a different user if someone logs out and then logs in with a different account.
I would do the following on the server for a user user and a token string token (assuming that only one user can be logged in on one device at a time):
Check if there is a Device for token_string.
If there is no device, create one for token_string and user.
If there is a device and its user is not user, update its user to user.
That way, the push notifications will be sent for the last user that logged in on the device.
Concerning your way of transforming the NSData to a hex string on the device, you should not rely on -[NSData description]. Better do it programmatically (typed in, not tested):
- (NSString *)hexStringForData:(NSData *)data
{
NSUInteger length = data.length;
const char *bytes = data.bytes;
NSMutableString *result = [NSMutableString stringWithCapacity:length * 2];
for (int i = 0; i < length; i++) {
[result appendFormat:#"%02x", bytes[i] & 0xff];
}
return [result copy];
}
I'll wager a guess at this one, but take it for what it's for, a guess.
When iOS devices are restored from backups, or when they are "restored" onto new devices, say, someone upgrading from a iPhone 4 to iPhone 5, or when someone gives their iPhone to their wife or sells it on eBay, you will get duplicated/redundant/confusing device data. I've definitely seen that happen, but not specifically with APNS tokens.
Here is what the APNS docs have to say about it:
By requesting the device token and passing it to the provider every
time your application launches, you help to ensure that the provider
has the current token for the device. If a user restores a backup to a
device or computer other than the one that the backup was created for
(for example, the user migrates data to a new device or computer), he
or she must launch the application at least once for it to receive
notifications again. If the user restores backup data to a new device
or computer, or reinstalls the operating system, the device token
changes. Moreover, never cache a device token and give that to your
provider; always get the token from the system whenever you need it.
If your application has previously registered, calling
registerForRemoteNotificationTypes: results in the operating system
passing the device token to the delegate immediately without incurring
additional overhead.
So, I'm not looking at your code, but it seems likely that your "duplicate" tokens have to do with some combination of not registering every time, some kind of caching, and device restoration.
It is important to understand what happens when a device registers. It sends your server a POST on the following:
/passkit/v1/devices/<deviceID>/registrations/<typeID>/<serial#>
And within the JSON payload is the push_token.
What matters is both the deviceID and push_token. As far as Apple is concerned, you communicate with the device through Apple's system with the push_token and nothing else.
As for the deviceID, it is the actual physical device that is used. The fact that the device might want to register multiple times is irrelevant, and your code should simply update the push_token in your DB based on the latest registration attempt. That's about it.

Resources