loading...

Correctly capture iOS 13 Device Token in Xamarin

codeprototype profile image Kevin Le ・3 min read

Cross posted on Medium

Capturing the Device Tokens is necessary for push notification to work. A Device Token is nothing but an ID that uniquely identifies a combination of a device and an app. So if we have 2 apps running on the same device, they will have different Device Tokens. Likewise, if the same app runs on 2 different devices, they will also have different Device Tokens. This is obvious because Apple Push Notification service (APNs) must know where to correctly push the notifications to.

To receive the Device Token, an app must register with APNs. I copied the following code from honestly-I-do-not-remember-where and paste in the AppDelegate class:

[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public string DeviceToken { get; set; }

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        ...
        LoadApplication(new App());
        ...
        return base.FinishedLaunching(app, options);
    }

    public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
    {
        DeviceToken = Regex.Replace(deviceToken.ToString(), "[^0-9a-zA-Z]+", "");
        ...        
    }

    public override void ReceivedLocalNotification(UIApplication application, UILocalNotification notification)
    {
        ...
    }

    public override void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo)
    {
        ...
    }
}

It worked great. I never bothered to question why or how.

Then comes trouble. iOS 13 arrived. Customers who updated their devices to iOS 13 no longer receive push notifications.

My boss informed me. Then I found this post on App forum and from NSHipster. It then makes all senses to me because the following line is returned by the toString() method in Xamarin C# NSData (equivalently, in Swift, that's the description field of the native Objective-C/ Swift NSData structure):

<124686a5 556a72ca d808f572 00c323b9 3eff9285 92445590 3225757d b83997ba>

So after we call Replace with the regex pattern [^0-9a-zA-Z] as in line

DeviceToken = Regex.Replace(deviceToken.ToString(), "[^0-9a-zA-Z]+", "");

We end up with the Device Token (if you count, that’s 32 bytes)

124686a5556a72cad808f57200c323b93eff9285924455903225757db83997ba

The trouble is: in iOS 13, the description field of the NSData structure now contains something like

{length = 32, bytes = 0xd3d997af 967d1f43 b405374a 13394d2f … 28f10282 14af515f }

Important: The elliptic (…) in the line above is NOT from my abbreviation. It is the ACTUAL string returned by APNs.

So obviously if we call Replace with the regex pattern [^0-9a-zA-Z] as above, we end up with a wrong Device Token:

length32bytes0xd3d997af967d1f43b405374a13394d2f28f1028214af515f

It got the junk length32bytes0x in the beginning, and it’s no longer 32 bytes in length.

The NSHipster article shows the fix in Swift:

let deviceTokenString = deviceToken.map { String(format: "%02x", $0) }.joined()

A nice one-line in Swift. We can do equally well in C#:

public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
    //DeviceToken = Regex.Replace(deviceToken.ToString(), "[^0-9a-zA-Z]+", "");
    //Replace the above line whick worked up to iOS12 with the code below:
    byte[] bytes = deviceToken.ToArray<byte>();
    string[] hexArray = bytes.Select(b => b.ToString("x2")).ToArray();
    DeviceToken = string.Join(string.Empty, hexArray);
}

Now let’s break this down:

  1. First we have to grab all the bytes in the device token by calling the ToArray() method on it.

  2. Once we have the bytes which is an array of bytes or, byte[], we call LINQ Select which applies an inner function that takes each byte and returns a zero-padded 2 digit Hex string. C# can do this nicely using the format specifier x2. The LINQ Select function returns an IEnumerable<string>, so it’s easy to call ToArray() to get an array of string or string[].

  3. Now just call Join() method on an array of string and we end up with a concatenated string.

Posted on Apr 7 by:

codeprototype profile

Kevin Le

@codeprototype

Driven by passion and patience

Discussion

markdown guide
 

Hello, Kevin, great article!

I have another question though.
I do need to recreate the NSData to update tags on azure. Before ios 13 i would do just this:
NSData nsData = new NSData(DeviceToken, NSDataBase64DecodingOptions.None);

that doesnt seems to work on ios13 since the NSData has a different structure, would you know how to do that too?

Thanks a lot!

 

What happens if you fail to capture the token on RegisteredForRemoteNotifications? Is there a way to get to token later? Or maybe the person originally denied push but later went into Settings and enabled it there. How do you know that happened and how do you get the token then?

 

That's an interesting scenario: user disable push notif, launch app, then user enable push. Question is in between, what does iOS pass when it calls RegisteredForRemoteNotifications. We will need to test. But that's common regardless of app being in Xamarin, Obj-C, Swift, etc.

 

Thank you for this excellent explanation of the problem and a succinct C# solution. It helped me a lot.

 

Got bit by this as well after upgrading to iOS 13.