Xamarin.Tip – iOS Push Notification Device Token Extraction in iOS 13+

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:

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.


11 thoughts on “Xamarin.Tip – iOS Push Notification Device Token Extraction in iOS 13+”

      1. 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

        Like

  1. 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.

    Like

    1. 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

      Like

  2. 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.

    Like

    1. 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

      Like

  3. 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.

    Like

  4. 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;

    Like

Leave a comment