Azure / azure-notificationhubs-dotnet

.NET SDK for Azure Notification Hubs
MIT License
70 stars 123 forks source link

[Question] Azure Notification Hub on MAUI #298

Open MouratidisA opened 1 year ago

MouratidisA commented 1 year ago

Question Is there a way to implement notification hub registration on MAUI applications (Android and iOS)?

I've tried implementing notification hub registration using NotificationHubClient on IOS( but I don't think a client application should use this implementation to register).

Implementation:

[Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")]
public async void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
 {
        //Create the client
        var nhClient = NotificationHubClient.CreateClientFromConnectionString(hubConnectionString,notificationHubName);
        //Set the notification tags for specific tag notifications
        string[] tags = { "sampletag" };
        //Retrieve the device token
        byte[] deviceTokenBytes = new byte[deviceToken.Length];
        System.Runtime.InteropServices.Marshal.Copy(deviceToken.Bytes, deviceTokenBytes, 0, (int)deviceToken.Length);
        //Convert token to string value
        string deviceTokenStr = BitConverter.ToString(deviceTokenBytes).Replace("-", string.Empty).Replace(" ", string.Empty);

        // Register
        AppleRegistrationDescription created = await 
        nhClient.CreateAppleNativeRegistrationAsync(deviceTokenStr,tags.ToArray());

        if (await nhClient.RegistrationExistsAsync(created.RegistrationId))
            Debug.WriteLine("Found the registration!!");
        else
            Debug.WriteLine("Registration not found");
 }

What is the proper way to implement push notifications on MAUI for Android and iOS using the Azure Notification hubs?

Denny966 commented 11 months ago

I'm having the same issue, how would one use it to register on the hub? Where are the examples?

lukampa commented 9 months ago

Same problem here..

adambarath commented 8 months ago

Any update on this?

michaelonz commented 7 months ago

Same issue - I see they have released 4.2.0-beta1 .... will this mean maui 8 is supported?

developer9969 commented 7 months ago

Why is it that is so difficult to get any kind of official response? Anyone on the Azure notificationHubs-dotnet team ? thanks

a-martsineuski commented 6 months ago

Is there are any updates or solutions? how to implement support of Notification Hub on MAUI?

michaelonz commented 6 months ago

We basically cant wait any longer for a response - does anyone know of a different notification framework that works for android and IOS that is being supported on maui?

developer9969 commented 6 months ago

@michaelonz that is big issue at the moment. there is nothing long term post june that will work on both iOS and android. and for people on this forum that moderate it and not to reply is just awful . I guess is to try onesignal (free) but does not work with hotrestart if you are using a pc.. or any paid alternative and would be nice to know if someone has implemented anything that works

gundetirevanth commented 6 months ago

Anyone from Azure notificationHubs-dotnet team respond on the above issue, its been long time that we are waiting for MAUI supported package of IOS Azure Push notification

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

DeveloperLookBook commented 6 months ago

Please, update your docs and examples and show how to use Azure Notification Hub with MAUI.

michaelonz commented 6 months ago

@RobertHedgate - What nuget packages did you have installed to make this work - I cant find the NotificationHubClient from the IOS platform code.

RobertHedgate commented 6 months ago

@michaelonz https://www.nuget.org/packages/Microsoft.Azure.NotificationHubs works for both android and iOS.

a-martsineuski commented 6 months ago

@RobertHedgate this package works for .NET 6, but not with the .NET8

RobertHedgate commented 6 months ago

@a-martsineuski My iOS app is on .net8. Doesn´t the .net6 flag mean .net6 or higher?

a-martsineuski commented 6 months ago

@RobertHedgate let's clarify: your app is net8.0-ios\net8.0-android? Yes, that means .NET6 or higher, but not .NETX-android.NETX-ios. It not possible to install it in the MAUI project

RobertHedgate commented 6 months ago

@a-martsineuski no it is MAUI app with platforms and all.

a-martsineuski commented 6 months ago

@RobertHedgate could you show a demo app, how did you install it?

RobertHedgate commented 6 months ago

@a-martsineuski I created a quick repo of my solution. It is not complete but contains where one gets the tokens and calls the azure hub. Can be found here https://github.com/RobertHedgate/MauiAppPush

DeveloperLookBook commented 6 months ago

@RobertHedgate I tried to run/build your test project as it is without any changes on Visual Studio for Mac 17.6.10 (build 428), and I get an error during the build proccess:

/Users/yevhenmyroshnychenko/Projects/Work/MauiAppPush/MauiAppPush: Error JAVA0000: Error in /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.jvm/1.3.0.2/buildTransitive/net7.0-android33.0/../../jar/androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class: Type androidx.collection.ArrayMapKt is defined multiple times: /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.jvm/1.3.0.2/buildTransitive/net7.0-android33.0/../../jar/androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class, /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.ktx/1.2.0.9/buildTransitive/net6.0-android31.0/../../jar/androidx.collection.collection-ktx.jar:androidx/collection/ArrayMapKt.class Compilation failed java.lang.RuntimeException: com.android.tools.r8.CompilationFailedException: Compilation failed to complete, origin: /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.jvm/1.3.0.2/buildTransitive/net7.0-android33.0/../../jar/androidx.collection.collection-jvm.jar androidx/collection/ArrayMapKt.class at com.android.tools.r8.utils.S0.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:135) at com.android.tools.r8.D8.main(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:5) Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete, origin: /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.jvm/1.3.0.2/buildTransitive/net7.0-android33.0/../../jar/androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class at Version.fakeStackEntry(Version_8.2.33.java:0) at com.android.tools.r8.T.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:5) at com.android.tools.r8.utils.S0.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:82) at com.android.tools.r8.utils.S0.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:32) at com.android.tools.r8.utils.S0.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:31) at com.android.tools.r8.utils.S0.b(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:2) at com.android.tools.r8.D8.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:42) at com.android.tools.r8.D8.b(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:13) at com.android.tools.r8.D8.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:40) at com.android.tools.r8.utils.S0.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:122) ... 1 more Caused by: com.android.tools.r8.utils.b: Type androidx.collection.ArrayMapKt is defined multiple times: /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.jvm/1.3.0.2/buildTransitive/net7.0-android33.0/../../jar/androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class, /Users/yevhenmyroshnychenko/.nuget/packages/xamarin.androidx.collection.ktx/1.2.0.9/buildTransitive/net6.0-android31.0/../../jar/androidx.collection.collection-ktx.jar:androidx/collection/ArrayMapKt.class at com.android.tools.r8.utils.Q2.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:21) at com.android.tools.r8.utils.D2.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:54) at com.android.tools.r8.utils.D2.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:10) at java.base/java.util.concurrent.ConcurrentHashMap.merge(ConcurrentHashMap.java:2048) at com.android.tools.r8.utils.D2.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:6) at com.android.tools.r8.graph.m4$a.d(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:6) at com.android.tools.r8.dex.c.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:61) at com.android.tools.r8.dex.c.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:12) at com.android.tools.r8.dex.c.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:9) at com.android.tools.r8.D8.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:45) at com.android.tools.r8.D8.d(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:17) at com.android.tools.r8.D8.c(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:69) at com.android.tools.r8.utils.S0.a(R8_8.2.33_429c93fd24a535127db6f4e2628eb18f2f978e02f99f55740728d6b22bef16dd:28) ... 6 more (JAVA0000) (MauiAppPush) java

RobertHedgate commented 6 months ago

@DeveloperLookBook I have added some nuget to android so it should build now but you still need to add google-sevice,json and all certificate to your project to make it work. You should more look at the project and use its registration to azure in your own projects.

michaelonz commented 6 months ago

Hi @RobertHedgate - can you please specify what certificates we need to add and where we need to put them in the folder structure along with if any properties are needed on the files (eg embedded resource etc etc) - so good to finally see an example working with .net8

RobertHedgate commented 6 months ago

@michaelonz Added this below to the readme of the project. Hope it helps.

For iOS you don´t need anything else in your app except selecting the correct certificate when you build.

Android you need the google-service.json file you download from you firebase console and add it directly under Android folder.

Here are a tutorial on iOS. https://learn.microsoft.com/en-us/azure/notification-hubs/ios-sdk-get-started

Setting up Android FCM V1 https://learn.microsoft.com/en-us/azure/notification-hubs/firebase-migration-rest

If GoogleServieJson doesn´t show up as build action https://github.com/dotnet/maui/issues/14486

michaelonz commented 5 months ago

@RobertHedgate - i downloaded your sample (thank you) and i downloaded my google-services.json and set the buildaction to googleservices.json.

When I run it on android (havent tried apple yet) I get the following error:

[FirebaseApp] Default FirebaseApp failed to initialize because no default options were found. This usually means that com.google.gms:google-services was not applied to your gradle project.

Any ideas?

NOTE: I have already tried re downloading the google-services.json file also.

michaelonz commented 5 months ago

Hi @RobertHedgate - No need for you to look into my issue above .... I worked it out. The in the .csproj file MUST match the project settings in firebase console. So on the Project settings/general tab - down scroll down to where it says "your apps" then "android apps" - the android app name MUST match - it will be something like "com.demo.sample" (eg com. company. product) I always assumed the connect string worked this out - but it must also check the ApplicationId

Hope this helps others also.

I still havent got the end to end message working but this resolved the following error:

[FirebaseApp] Default FirebaseApp failed to initialize because no default options were found. This usually means that com.google.gms:google-services was not applied to your gradle project.

DavidMarquezF commented 1 month ago

I repost my comment from https://github.com/Azure/azure-notificationhubs-xamarin/issues/125, just in case it is useful for anyone:

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)