This weekend, I spent some time getting some apps updated with the latest Xamarin, iOS, Android, etc. A lot of these apps make use of push notifications across platforms. I was running some automated tests against them to do the following steps:
- Register for push notifications on sign-in / startup
- Send registration update details to API
- API sends updates to Azure Notification Hub or Firebase (depends on the project)
- Send push notification to all test users
- Send push notification to specific test user
I noticed immediately that all my new iOS builds were failing to receive any push notifications after updating registrations.
Time to debug!
The Old Way
Here was my original method for kicking off the push notification registration:
AppDelegate.cs
//... private void RegisterForPush() { if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0)) { UNUserNotificationCenter.Current.RequestAuthorization(UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound, (granted, error) => { if (granted) InvokeOnMainThread(UIApplication.SharedApplication.RegisterForRemoteNotifications); }); } else if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) { var pushSettings = UIUserNotificationSettings.GetSettingsForTypes( UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound, new NSSet()); UIApplication.SharedApplication.RegisterUserNotificationSettings(pushSettings); UIApplication.SharedApplication.RegisterForRemoteNotifications(); } else { UIRemoteNotificationType notificationTypes = UIRemoteNotificationType.Alert | UIRemoteNotificationType.Badge | UIRemoteNotificationType.Sound; UIApplication.SharedApplication.RegisterForRemoteNotificationTypes(notificationTypes); } } // ..
This processes kicks off the internal iOS process to start registration. It will check against your apps registered entitlements for push, check the environment it should be using between development
and production
, and then if all goes well, it will hit the RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
method in the AppDelegate
. If there are any explicit errors it will instead hit FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
.
So what we need is the data within that deviceToken
of the RegisteredForRemoteNotifications
method to send to our API.
Here is what the ORIGINAL implementation looked like (with some of the abstractions removed for simplicity):
AppDelegate.cs
// non-user registration public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { // if using default registration with notification hub for non-user apps Hub = new SBNotificationHub("{CONN_STRING}", "{AZURE_HUB_NAME}"); Hub.UnregisterAll(deviceToken, (error) => { if (error != null) { System.Diagnostics.Debug.WriteLine("Error calling Unregister: {0}", error.ToString()); return; } NSSet tags = null; // create tags if you want Hub.RegisterNativeAsync(deviceToken, tags); }); }
And for handling specific user registration, we need to send it to the API:
AppDelegate.cs
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { var token = deviceToken?.Description?.Trim('<', '>')?.Replace(" ", string.Empty); if (!string.IsNullOrEmpty(token)) { // this sends the handle to the API to do all the registration var remoteRegistrar = new RemoteNotificationRegistrar(); remoteRegistrar.RegisterForPushAsync("apns", token); } }
Debugging showed me the issue was here:
var token = deviceToken?.Description?.Trim('<', '>')?.Replace(" ", string.Empty);
The token was no longer in the same format… Feeling like an idiot who has been out of the loop, I hit the internet to see what I missed and landed on some of these lovely articles and posts:
- https://onesignal.com/blog/ios-13-introduces-4-breaking-changes-to-notifications/
- https://forums.developer.apple.com/thread/117545
- https://nshipster.com/apns-device-tokens/#overturned-in-ios-13
So lets do this update in Xamarin:
The New Way
So the old token structure looked like this:
“
And the new token looks like this:
{length = 32, bytes = 0x965b251c 6cb1926d e3cb366f dfb16ddd ... 5f857679 376eab7c }
So basically we need a clean version of that bytes
property of the NSData
object.
Azure notification hubs already do this behind the scenes, but we need the clean version for our own method to send to our API, so here’s the new version:
AppDelegate.cs
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { var token = ExtractToken(deviceToken); if (!string.IsNullOrEmpty(token)) { // this sends the handle to the API to do all the registration var remoteRegistrar = new RemoteNotificationRegistrar(); remoteRegistrar.RegisterForPushAsync("apns", token); } } private string ExtractToken(NSData deviceToken) { if (deviceToken.Length == 0) return null; var result = new byte[deviceToken.Length]; System.Runtime.InteropServices.Marshal.Copy(deviceToken.Bytes, result, 0, (int)deviceToken.Length); return BitConverter.ToString(result).Replace("-", ""); }
There are a few different ways to write this ExtractToken
method, but this is what has worked for me!.
After making this change and running it on iOS 13, I started getting user notifications again!
Both Ways
This is great and all, but I still want to make sure folks who haven’t updated to iOS 13 can still use my apps, so I added some explicit version checks to decide which way to extract the token. Here’s what the final looks like all within that ExtractToken
method:
private string ExtractToken(NSData deviceToken) { if(UIDevice.CurrentDevice.CheckSystemVersion(13,0)) { if(deviceToken.Length > 0) { var result = new byte[deviceToken.Length]; Marshal.Copy(deviceToken.Bytes, result, 0, (int)deviceToken.Length); return BitConverter.ToString(result).Replace("-", string.Empty); } } else { return deviceToken?.Description?.Trim('<', '>')?.Replace(" ", string.Empty); } return null; }
And that’s it! Try this out if you’re having issues with the latest iOS 13 update and your push notification registration.
If you’re still having problems with push notification registration in general, leave a comment below! I’d love to put together an article about the common pitfalls of push notifications on both iOS and Android with a checklist of things to look at, fix, or build on top of what you have!
If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile and AI developer tips and tricks!
Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.
Hi Alex! Thanks for your article.
I enjoyed reading it since I’ve not ran into this kind of issue yet.
I personally like to let appcenter.ms handle my push eco-system.
It’s very practical and their api and swagger https://openapi.appcenter.ms/ is very nice.
Cheers ♥
LikeLiked by 1 person
Hope you dont like it too much! https://devblogs.microsoft.com/appcenter/app-center-mbaas-retirement/
You need to migrate soon
LikeLike
Luckily the migration guide to azure notification hubs is pretty solid. It’s sad – I really enjoyed the push and data side of appcenter for simple apps to spin up real quick
LikeLike
In the “both ways” paragraph, ExtractToken function, line 14 I think you forgot to add the
?.Replace(” “, string.Empty)
for the old way prior to ios 13. The new extraction method will not contain spaces and the old shouldn’t either.
LikeLike
Good catch. The “New way” doesn’t need the explicit removal since the bit converter doesn’t actually use the spaces seen in the data structure and instead uses “-” which is why we rip those out. I’ll update the “both ways” to include the whitespace stripping in the old way
LikeLike
You can use more simplest way:
“`
var bytes = deviceToken.ToArray();
return BitConverter.ToString(bytes).Replace(“-“, string.Empty);
“`
LikeLike
Hi! Thanks for your great article.
One question tho, how can I get back the string to NSData if I were to use Hub.RegisterNativeAsync method to register a tag in the future? I am planning to store the string in local database.
LikeLike
Depends on what you need. If it’s just the string of the token then you can store the NSString of it. If it’s the whole thing then just use the NSData right when you have it in that method. I use xamarin essentials to store the extracted string, personally
LikeLike
Hi Alex, thanks for show a solution for this PN problem. But I think you are using not the retired AppCenter PN right? Instead I see that you use the Azure Notification Hubs, right? Do you know if there is a solution for the AppCenterPush service? thanks.
LikeLike
This solution is agnostic to what push service you use. Same thing will work for app center push since it expects the same token structure. However, app center push is being retired and deprecated and shortly won’t work at all https://visualstudiomagazine.com/articles/2020/02/14/app-center-mbaas.aspx?m=1
LikeLike
Funny to see you have been on the same journey as I have. I was looking for a solution to get the token after `ReceivedRemoteNotification` has been called, so I can register new templates in my backend. I also ended up just caching the token like you have and using it to register new templates.
On Android its easier as you can get the token at any time.
var instanceIdResult = await FirebaseInstanceId.Instance.GetInstanceId().AsAsync();
var token = instanceIdResult?.Token;
LikeLike