Forum Xamarin.iOS

Xamarin iOS Azure Notification Hubs with Xcode 11.3 and iOS 13.3 Issue

Hey we're hitting this issue on all devices with iOS v13.3.1 with Xamarin IOS. It seems to be related to a change in the format for the device token:

iOS 13.3:
{length = 32, bytes = 0xf1d219ab c4aab6f4 e0408ae2 8a6e9a5c ... c6ddccbb 6a63fe2f }

iOS 12:

We are unsure if there is an underlying issue with the SDK, or out of our own fault, but we are using Xamarin.Azure.NotificationHubs.IOS v2.0.4. Registration happens client side on the device.

Originally we had this:

        public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
        {
            _notificationDelegate.Register(_user.UserId, deviceToken);
        }
public void Register(string userID, NSData deviceToken)
        {
            _hub = new SBNotificationHub(
               Configuration.Configuration.NOTIFICATION_HUB_CONNECTION_STRING,
               Configuration.Configuration.NOTIFICATION_HUB_NAME);

            _hub.UnregisterAll(deviceToken, (unregisterError) => {
                if (unregisterError != null)
                {
                    _handler.LogError($"UnregisterAll error: {unregisterError.ToString()}");
                    return;
                }

                _handler.LogInfo($"UnregisterAll complete for user: {userID}. Starting RegisterNative");

                var tags = new NSSet($"username:{userID}");
                _hub.RegisterNative(deviceToken, tags, (registerError) =>
                {
                    if (registerError != null)
                    {
                        _handler.LogError($"RegisterNative error: {registerError}");
                    }
                    else
                    {
                        _handler.LogInfo($"RegisterNative registered: {deviceToken} for user ID: {userID}");
                        IsRegistered = true;
                    }
                });
            });
        }

This does not work anymore. RegisterNative does not get called so the device never appears in the notification hub. Given the new formats and form posts I've read online, the new code is as follows:

 public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
        {
            byte[] token = deviceToken.ToArray();
            string registrationToken = BitConverter.ToString(token).Replace("-", "");

            _notificationDelegate.Register(_user.UserId, NSData.FromString(registrationToken));
        }

In this case, the device gets registered but is deleted immediately on using the TEST SEND feature in azure notification hubs, presumably because the registration is actually invalid with the conversion. Is this a valid way to register iOS 13.3 devices with Notification Hubs, or is something else up? Note: our iOS 13.1 devices running the app built against Xcode 10 do work, and so do our iOS 12 devices.

Best Answer

  • Accepted Answer

    Hi @ColeX,

    This doesn't seem to work either. The call to UnregisterAllAsync never completes and no exceptions are thrown. At least in the synchronous case they execute. However, we got this working after a couple days of madness.

    TL;DR:

    This works (or used to):

    • iOS 13 and all releases before
    • App built with Xcode 10
    • Certificate-based authentication in Azure Notification Hubs
    • Xamarin.Azure.NotificationHubs.iOS v1.2.5.2

    This also works (and currently still does):

    • iOS 13 and all releases before
    • App built with Xcode 11
    • Token-based authentication in Azure Notification Hubs
    • Xamarin.Azure.NotificationHubs.iOS v2.0.4

    Gotchas

    • No other configuration of resources/SDKs worked. We tried a matrix, a very large matrix, and they don't work.
    • The same token key cannot be used for both production and sandbox in the Notification Hub UI. Also, switching them breaks the other environment.

    If you're interested

    We had an iOS 13.1 device receiving notifications in December. We released a couple new version of our app, and they haven't gotten notifications since, but they only noticed after we released several newer version.

    At the time we were still on Xamarin.Azure.NotificationHubs.iOS v1.2.5.2, so we thought, "Oh something is wrong with iOS 13? just roll back to a previous version". This didn't work because they didn't roll back far enough.

    So we tried the bit conversion method below:

    byte[] token = deviceToken.ToArray();
    string registrationToken = BitConverter.ToString(token).Replace("-", "");
    

    We handed off this string to RegisterNativeAsync from V1.2.5.2 and the device was registered in notification hubs. When we tried to do a test send, it passed, but removed the registration and silently failed for iOS 13 devices only. iOS 12 devices worked with the conversion.

    After that, we scratched our heads (not literally), and updated to V2.0.4. We kept the conversion and tried again. Same issue: The registration exists until you try to send something to it.

    Then we looked at the documentation and saw we were missing the call to RegisterTemplate which was not necessary in V1.2.5.2. We still don't actually know what this really does and why it is in the documentation now but we will be investigating more later. Another migration issue. We added this. Still the same issue.

    Then we remembered that, sometime in the last month, we updated our build machines to Catalina and XCode 11. This is what actually broke everything (presumably), however it is not immediately clear why updating to a newer version of Xcode should have an impact on the validity of a registration, given that we were already doing the conversion.

    We came across this statement in the Azure documentation:

    "Apple now requires that developers identify notifications as an alert or background notifications through the new apns-push-type header in the APNS API. According to Apple's documentation: "The value of this header must accurately reflect the contents of your notification's payload. If there is a mismatch, or if the header is missing on required systems, APNs may return an error, delay the delivery of the notification, or drop it altogether."

    We thought "but this doesn't explain why the iOS 13 device isn't receiving notifications." However, maybe it affects new registrations? Additionally, the documentation states:

    "Remember that you must configure Azure Notification Hubs to use token-based authentication to set the required header; for more information, see Token-based (HTTP/2) Authentication for APNS."

    We were still using cert-based auth. We decided to switch to token based authentication and the new registrations were happy.

    I guess the issues came about from multiple solutions around mutliple different versions. We are not doing the bit conversion on either device, and everything seems to be okay.

    So far.

    @ColeX no further triage needed however, if you know anyone on the Xamarin/Hubs team, I'd love to talk with them and work on the documentation that's currently available since it has a number of mistakes, is fragmented, and is omits quite a few requirements for V1.x -> V2.x migration.

    The final code:

    public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
    {
        try
        {
            var successfulRegistration = _notificationDelegate.Register(_user.UserId, deviceToken).Result;
            if (!successfulRegistration)
            {
                /// log this
            }
        }
        catch(Exception e)
        {
            /// log this
        }
    }
    
    /// originally this used the async methods from V2.0.4.
    /// The never returned, even with .ConfigureAwait(false)
    /// The method signature retains Task<bool> even though it is not required
    public async Task<bool> Register(string userID, NSData deviceToken)
    {
        var error = false;
        try
        {
            _hub = new SBNotificationHub(
                Configuration.Configuration.NOTIFICATION_HUB_CONNECTION_STRING,
                Configuration.Configuration.NOTIFICATION_HUB_NAME);
    
            /// we add a username with our registrations.
            var tags = new NSSet(CloudNotificationConstants.DefaultSubscriptionTag, $"username:{userID}");
    
            _hub.UnregisterAll(deviceToken, (errorCallback) => {
                if (errorCallback != null)
                {
                    /// log this
                    error = true;
                }
            });
    
            if (error) return !error;
    
            _hub.RegisterNative(deviceToken, tags, (errorCallback) =>
            {
                if (errorCallback != null)
                {
                    /// log this
                    error = true;
                }
            });
    
            if (error) return !error;
    
            _hub.RegisterTemplate(deviceToken,
                CloudNotificationConstants.DefaultTemplateName,
                CloudNotificationConstants.DefaultTemplate,
                CloudNotificationConstants.DefaultExpiration,
                tags, (errorCallback) =>
                {
                    if (errorCallback != null)
                    {
                        /// log this
                        error = true;
                    }
                });
    
            if (error) return !error;
    
            /// private variable for our safe keeping. Not necessary
            IsRegistered = true;
            return IsRegistered;
        }
        catch(Exception e)
        {
            /// originally this caught TASK exceptions. A regular exception will do now.
            if (e is AggregateException && e.InnerException != null)
            _handler.LogException(e.InnerException);
        }
    
        return false;
    }
    
    /// We may want multiple templates, so we'll put them in a dictionary for now
    public static class CloudNotificationConstants
    {
        /// <summary>
        /// Subscription tags
        /// </summary>
        public const string DefaultSubscriptionTag = "default";
    
        /// <summary>
        /// Default notification template name
        /// </summary>
        public const string DefaultTemplateName = "defaultTemplate";
    
        public static string DefaultTemplate => Templates[DefaultTemplateName];
    
        /// <summary>
        /// The default expiration time for iOS devices
        /// </summary>
        /// <remarks> This expiration date may not actually be necessary
        public static string DefaultExpiration = DateTime.UtcNow.AddDays(365).ToString(CultureInfo.CreateSpecificCulture("en-US"));
    
        /// <summary>
        /// Default notification template
        /// </summary>
        private const string DefaultTemplateValue = "{\"aps\":{\"alert\":\"$(messageParam)\"}}";
    
        /// <summary>
        /// All notification templates
        /// </summary>
        private static Dictionary<string, string> Templates = new Dictionary<string, string>()
        {
            {DefaultTemplateName, DefaultTemplateValue}
        };
    }
    

Answers

  • ColeXColeX Member, Xamarin Team Xamurai

    Try the following code

      try
         {
               AzureNotifHub = new SBNotificationHub(HUB_LISTENSHARED_CONNSTRING, HUBNAME);
    
              // update registration with Azure Notification Hub
               await AzureNotifHub.UnregisterAllAsync(deviceToken);
    
    
               var tags = new NSSet(AppConstants.IOSSUBSCRIPTIONTAGS.ToArray());
               await AzureNotifHub.RegisterNativeAsync(deviceToken, tags);
    
               var templateExpiration = DateTime.Now.AddDays(120).ToString(System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
                        await AzureNotifHub.RegisterTemplateAsync(deviceToken, "defaultTemplate", AppConstants.IOS_APNS_TEMPLATE_BODY, templateExpiration, tags);
          }
          catch (Exception ex)
          {
               Debug.WriteLine(ex.Message);
          }
    

    Refer https://stackoverflow.com/a/58280762/8187800.

  • gadeweevergadeweever Member
    Accepted Answer

    Hi @ColeX,

    This doesn't seem to work either. The call to UnregisterAllAsync never completes and no exceptions are thrown. At least in the synchronous case they execute. However, we got this working after a couple days of madness.

    TL;DR:

    This works (or used to):

    • iOS 13 and all releases before
    • App built with Xcode 10
    • Certificate-based authentication in Azure Notification Hubs
    • Xamarin.Azure.NotificationHubs.iOS v1.2.5.2

    This also works (and currently still does):

    • iOS 13 and all releases before
    • App built with Xcode 11
    • Token-based authentication in Azure Notification Hubs
    • Xamarin.Azure.NotificationHubs.iOS v2.0.4

    Gotchas

    • No other configuration of resources/SDKs worked. We tried a matrix, a very large matrix, and they don't work.
    • The same token key cannot be used for both production and sandbox in the Notification Hub UI. Also, switching them breaks the other environment.

    If you're interested

    We had an iOS 13.1 device receiving notifications in December. We released a couple new version of our app, and they haven't gotten notifications since, but they only noticed after we released several newer version.

    At the time we were still on Xamarin.Azure.NotificationHubs.iOS v1.2.5.2, so we thought, "Oh something is wrong with iOS 13? just roll back to a previous version". This didn't work because they didn't roll back far enough.

    So we tried the bit conversion method below:

    byte[] token = deviceToken.ToArray();
    string registrationToken = BitConverter.ToString(token).Replace("-", "");
    

    We handed off this string to RegisterNativeAsync from V1.2.5.2 and the device was registered in notification hubs. When we tried to do a test send, it passed, but removed the registration and silently failed for iOS 13 devices only. iOS 12 devices worked with the conversion.

    After that, we scratched our heads (not literally), and updated to V2.0.4. We kept the conversion and tried again. Same issue: The registration exists until you try to send something to it.

    Then we looked at the documentation and saw we were missing the call to RegisterTemplate which was not necessary in V1.2.5.2. We still don't actually know what this really does and why it is in the documentation now but we will be investigating more later. Another migration issue. We added this. Still the same issue.

    Then we remembered that, sometime in the last month, we updated our build machines to Catalina and XCode 11. This is what actually broke everything (presumably), however it is not immediately clear why updating to a newer version of Xcode should have an impact on the validity of a registration, given that we were already doing the conversion.

    We came across this statement in the Azure documentation:

    "Apple now requires that developers identify notifications as an alert or background notifications through the new apns-push-type header in the APNS API. According to Apple's documentation: "The value of this header must accurately reflect the contents of your notification's payload. If there is a mismatch, or if the header is missing on required systems, APNs may return an error, delay the delivery of the notification, or drop it altogether."

    We thought "but this doesn't explain why the iOS 13 device isn't receiving notifications." However, maybe it affects new registrations? Additionally, the documentation states:

    "Remember that you must configure Azure Notification Hubs to use token-based authentication to set the required header; for more information, see Token-based (HTTP/2) Authentication for APNS."

    We were still using cert-based auth. We decided to switch to token based authentication and the new registrations were happy.

    I guess the issues came about from multiple solutions around mutliple different versions. We are not doing the bit conversion on either device, and everything seems to be okay.

    So far.

    @ColeX no further triage needed however, if you know anyone on the Xamarin/Hubs team, I'd love to talk with them and work on the documentation that's currently available since it has a number of mistakes, is fragmented, and is omits quite a few requirements for V1.x -> V2.x migration.

    The final code:

    public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
    {
        try
        {
            var successfulRegistration = _notificationDelegate.Register(_user.UserId, deviceToken).Result;
            if (!successfulRegistration)
            {
                /// log this
            }
        }
        catch(Exception e)
        {
            /// log this
        }
    }
    
    /// originally this used the async methods from V2.0.4.
    /// The never returned, even with .ConfigureAwait(false)
    /// The method signature retains Task<bool> even though it is not required
    public async Task<bool> Register(string userID, NSData deviceToken)
    {
        var error = false;
        try
        {
            _hub = new SBNotificationHub(
                Configuration.Configuration.NOTIFICATION_HUB_CONNECTION_STRING,
                Configuration.Configuration.NOTIFICATION_HUB_NAME);
    
            /// we add a username with our registrations.
            var tags = new NSSet(CloudNotificationConstants.DefaultSubscriptionTag, $"username:{userID}");
    
            _hub.UnregisterAll(deviceToken, (errorCallback) => {
                if (errorCallback != null)
                {
                    /// log this
                    error = true;
                }
            });
    
            if (error) return !error;
    
            _hub.RegisterNative(deviceToken, tags, (errorCallback) =>
            {
                if (errorCallback != null)
                {
                    /// log this
                    error = true;
                }
            });
    
            if (error) return !error;
    
            _hub.RegisterTemplate(deviceToken,
                CloudNotificationConstants.DefaultTemplateName,
                CloudNotificationConstants.DefaultTemplate,
                CloudNotificationConstants.DefaultExpiration,
                tags, (errorCallback) =>
                {
                    if (errorCallback != null)
                    {
                        /// log this
                        error = true;
                    }
                });
    
            if (error) return !error;
    
            /// private variable for our safe keeping. Not necessary
            IsRegistered = true;
            return IsRegistered;
        }
        catch(Exception e)
        {
            /// originally this caught TASK exceptions. A regular exception will do now.
            if (e is AggregateException && e.InnerException != null)
            _handler.LogException(e.InnerException);
        }
    
        return false;
    }
    
    /// We may want multiple templates, so we'll put them in a dictionary for now
    public static class CloudNotificationConstants
    {
        /// <summary>
        /// Subscription tags
        /// </summary>
        public const string DefaultSubscriptionTag = "default";
    
        /// <summary>
        /// Default notification template name
        /// </summary>
        public const string DefaultTemplateName = "defaultTemplate";
    
        public static string DefaultTemplate => Templates[DefaultTemplateName];
    
        /// <summary>
        /// The default expiration time for iOS devices
        /// </summary>
        /// <remarks> This expiration date may not actually be necessary
        public static string DefaultExpiration = DateTime.UtcNow.AddDays(365).ToString(CultureInfo.CreateSpecificCulture("en-US"));
    
        /// <summary>
        /// Default notification template
        /// </summary>
        private const string DefaultTemplateValue = "{\"aps\":{\"alert\":\"$(messageParam)\"}}";
    
        /// <summary>
        /// All notification templates
        /// </summary>
        private static Dictionary<string, string> Templates = new Dictionary<string, string>()
        {
            {DefaultTemplateName, DefaultTemplateValue}
        };
    }
    
Sign In or Register to comment.