Azure / azure-notificationhubs-xamarin

Azure Notification Hubs Sample for Xamarin Forms
MIT License
35 stars 23 forks source link

How to use Xamarin.Azure.NotificationHubs.iOS with MAUI ? #125

Open punio opened 1 year ago

punio commented 1 year ago

Question I used Xamarin.Azure.NotificationHubs.iOS with Xamarin.Forms, but with MAUI NuGet Package installation shows NU1202 and I can't install.

Why is this not a Bug or a feature Request? I'm not very familiar with MAUI, but it seems that most of the Xamarin libraries can be used with MAUI.

Setup (please complete the following information if applicable):

FM1973 commented 1 year ago

I got the same problem. @punio did you find a solution?

punio commented 1 year ago

@FM1973 I found only out that can't use it as it is with MAUI. It may be possible with Shiny, but I haven't tried it yet.

FM1973 commented 1 year ago

@punio I took another road. I´m now using Azure NotificationHub only on the server side. On the client side I did the implementation like described in the following link: https://learn.microsoft.com/en-us/azure/developer/mobile-apps/notification-hubs-backend-service-xamarin-forms#configure-the-native-android-project-for-push-notifications

Although I changed some bits and replaced deprecated code with the code one should use now.

Works pretty well...

punio commented 1 year ago

@FM1973 Thank you for letting me know how to solve it. I will try that solution too.

eyeveye commented 1 year ago

@punio I took another road. I´m now using Azure NotificationHub only on the server side. On the client side I did the implementation like described in the following link: https://learn.microsoft.com/en-us/azure/developer/mobile-apps/notification-hubs-backend-service-xamarin-forms#configure-the-native-android-project-for-push-notifications

Although I changed some bits and replaced deprecated code with the code one should use now.

Works pretty well...

Hi, do you get the IOS working or android version? If is IOS, can you share how you achieve it?

fdhsdrdark commented 1 year ago

According to this https://github.com/xamarin/XamarinComponents/issues/1418 Xamarin.Azure.NotificationHubs.iOS is not going to be further supported! So currently, what @FM1973 suggested seems the only solution, not only for MAUI but for .Net iOS in .Net 6 also. Any input is more than welcome!

RobertHedgate commented 6 months ago

I solved iOS push by using Microsoft.Azure.NotificationHubs

var hub = NotificationHubClient.CreateClientFromConnectionString(this.connectionString, this.notificationHubName); var installation = new Installation { InstallationId = deviceToken, PushChannel = deviceToken, Platform = NotificationPlatform.Apns, Tags = Array.Empty() };

await hub.CreateOrUpdateInstallationAsync(installation);

deviceToken is obtained from RegisteredForRemoteNotifications

rimex-bkimmett commented 6 months ago

Yeah, I've just finished migrating my app to Microsoft.Azure.NotificationHubs as well. I just need to get the token from APNS (or Firebase on Android) and feed that to the Installation request.

RobertHedgate commented 6 months ago

@rimex-bkimmett Please see this thread https://github.com/Azure/azure-notificationhubs-dotnet/issues/298. I have created a boiler plate repo (https://github.com/RobertHedgate/MauiAppPush) on how I got it to work but more questions are answered is that thread,

DavidMarquezF commented 2 months ago

Now it's explained in the docs: https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/push-notifications?view=net-maui-8.0#create-a-net-maui-app

They show how to do it with api key instead of ListenConnectionString which has some benefits but of course adds some complexity. @RobertHedgate proposal still looks like the one with the least amount of boilerplate code to make everything work.

I believe the only "issue" with @RobertHedgate proposal is that it doensn't refresh the azure hub installation when the token changes in the onNewToken function.

In my case, I did a hybrid between both. I followed the tutorial, but my NotificationRegistrationService (the one that they use the api key and the http calls) is like this:

   public class NotificationRegistrationService : INotificationRegistrationService
   {
       const string CachedDeviceTokenKey = "cached_notification_hub_device_token";
       const string CachedTagsKey = "cached_notification_hub_tags";
       const string CachedUserIdKey = "cached_notification_hub_user_id";

       IDeviceInstallationService _deviceInstallationService;
       private NotificationHubClient _hub;

       public NotificationRegistrationService(IDeviceInstallationService deviceInstallationService)
       {
           _deviceInstallationService = deviceInstallationService ?? throw new ArgumentNullException(nameof(deviceInstallationService));
           _hub = NotificationHubClient.CreateClientFromConnectionString(Config.ListenConnectionString, Config.NotificationHubName);
       }

       public async Task DeregisterDeviceAsync()
       {
           var cachedToken = await SecureStorage.GetAsync(CachedDeviceTokenKey)
               .ConfigureAwait(false);

           if (cachedToken == null)
               return;

           var deviceId = GetDeviceId();

           await _hub.DeleteInstallationAsync(deviceId);

           SecureStorage.Remove(CachedDeviceTokenKey);
           SecureStorage.Remove(CachedTagsKey);
           SecureStorage.Remove(CachedUserIdKey);

       }

       public async Task RegisterDeviceAsync(string userId, params string[] tags)
       {
           var deviceInstallation = _deviceInstallationService?.GetDeviceInstallation(tags);

           if (!string.IsNullOrEmpty(userId))
               deviceInstallation.UserId = userId;

           await _hub.CreateOrUpdateInstallationAsync(deviceInstallation);

           await SecureStorage.SetAsync(CachedDeviceTokenKey, deviceInstallation.PushChannel)
               .ConfigureAwait(false);

           await SecureStorage.SetAsync(CachedTagsKey, JsonSerializer.Serialize(tags));
           await SecureStorage.SetAsync(CachedUserIdKey, userId);
       }

       public async Task RefreshRegistrationAsync()
       {
           var cachedToken = await SecureStorage.GetAsync(CachedDeviceTokenKey)
               .ConfigureAwait(false);

           var serializedTags = await SecureStorage.GetAsync(CachedTagsKey)
               .ConfigureAwait(false);

           var cachedUserId = await SecureStorage.GetAsync(CachedUserIdKey)
              .ConfigureAwait(false);

           if (string.IsNullOrWhiteSpace(cachedToken) ||
               string.IsNullOrWhiteSpace(serializedTags) ||
               string.IsNullOrEmpty(cachedUserId) ||
               string.IsNullOrWhiteSpace(_deviceInstallationService.Token) ||
               cachedToken == _deviceInstallationService.Token)
               return;

           var tags = JsonSerializer.Deserialize<string[]>(serializedTags);

           await RegisterDeviceAsync(cachedUserId, tags);
       }

       private string _userIdPropName;
       private string UserIdPropName => _userIdPropName ??= "/" + GetJsonPropertyName(typeof(Installation), nameof(Installation.UserId));

       private string _tagsPropName;
       private string TagsPropName => _tagsPropName ??= GetJsonPropertyName(typeof(Installation), nameof(Installation.Tags));

       public async Task UpdateUserId(string userName)
       {

           var deviceId = GetDeviceId();

           var updates = new List<PartialUpdateOperation>
           {
               new() { Operation = UpdateOperationType.Replace, Path = UserIdPropName, Value = userName },
               await GetUpdateTagOperation(new[] { (NotificationTags.Username, userName) })
           };

           await _hub.PatchInstallationAsync(deviceId, updates);
       }

       public async Task UpdateTag(NotificationTags tag, string value)
       {
           var deviceId = GetDeviceId();

           var updates = new List<PartialUpdateOperation>
           {
               await GetUpdateTagOperation(new[] { (tag, value) })
           };

           await _hub.PatchInstallationAsync(deviceId, updates);
       }

       private async Task<PartialUpdateOperation> GetUpdateTagOperation(IEnumerable<(NotificationTags, string)> newTags)
       {
           var serializedTags = await SecureStorage.GetAsync(CachedTagsKey)
              .ConfigureAwait(false);

           var tags = new List<string>();

           if (!string.IsNullOrWhiteSpace(serializedTags))
               tags.AddRange(JsonSerializer.Deserialize<string[]>(serializedTags));

           foreach (var (tagType, value) in newTags)
           {
               var tagId = NotificationHubUtils.GetTagId(tagType);

               var tagIndex = tags.FindIndex(a => a.StartsWith(tagId));
               var tag = NotificationHubUtils.GetTag(tagType, value);

               if (tagIndex >= 0)
                   tags[tagIndex] = tag;
               else
                   tags.Add(tag);
           }

           return new() { Operation = UpdateOperationType.Replace, Path = TagsPropName, Value = JsonSerializer.Serialize(tags) };
       }

       private string GetDeviceId()
       {
           var deviceId = _deviceInstallationService?.GetDeviceId();

           if (string.IsNullOrWhiteSpace(deviceId))
               throw new Exception("Unable to resolve an ID for the device.");

           return deviceId;
       }

       private static string GetJsonPropertyName(Type type, string propertyName)
       {
           if (type is null)
               throw new ArgumentNullException(nameof(type));

           return type.GetProperty(propertyName)
               ?.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()
               ?.PropertyName;
       }
   }

:warning: I think currently there is an issue with their Android implementation in their example. They implement Android.Gms.Tasks.IOnSuccessListener on the Main activity, but they don't add it as a listener to anything. My guess is that they wanted to add what in java is like: FirebaseMessaging.getInstance().getToken().addOnCompleteListener(IOnSuccessListener). The solution is to simply set the DeviceInstallation.Token to Firebase.Instance.getToken() before the RegisterDevice function is called (Firebase docs recommend updating it in the OnCreate function)