TobiasBuchholz / Plugin.Firebase

Wrapper around the native Android and iOS Firebase Xamarin SDKs
MIT License
211 stars 49 forks source link

iOS/Android Notification Permissions #175

Closed axylophon closed 1 year ago

axylophon commented 1 year ago

Hi,

i have a question regarding the iOS notification permission request. As I understand this is done in the CheckIfValidAsync method:

public Task CheckIfValidAsync()
{
    if(UIDevice.CurrentDevice.CheckSystemVersion(10, 0)) {
        RequestAuthorization();
    }
    return Task.CompletedTask;
}

private void RequestAuthorization()
{
    UNUserNotificationCenter.Current.RequestAuthorization(
        UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,
        (granted, error) => {
            if(!granted) {
                Error?.Invoke(this, new FCMErrorEventArgs("User permission for remote notifications is not granted"));
            }
        });
}

With Android 13 the permissions must be requested manually - i solved this via a custom permission class as maui does not support it currently:

public class Notification : Microsoft.Maui.ApplicationModel.Permissions.BasePlatformPermission
{
#if ANDROID
    public override (string androidPermission, bool isRuntime)[] RequiredPermissions =>
        new List<(string androidPermission, bool isRuntime)>
        {
            (Android.Manifest.Permission.PostNotifications, true),
        }.ToArray();
#elif IOS
#endif

Usage with the permissions api. This is working great:

var status = await Microsoft.Maui.ApplicationModel.Permissions.CheckStatusAsync<Notification>();
status = await Microsoft.Maui.ApplicationModel.Permissions.RequestAsync<Notification>();

How can i achieve the same behaviour for iOS to request the permission manually in the code or check if the permission for notifications is granted or not? I do not want to do it on application start but when the user activates notifications in our app.

Therefore it would be nice to build an async method to do this to block the UI until the permission dialog is closed like in the android example.

Should i skip calling CheckIfValidAsync on iOS if i want to do it on my own?

Is it possible to request the Push Token without the granted permission?

andyzukunft commented 1 year ago

Hey axylophon,

for iOS you need to set the regular iOS permissions to Entitlements.plist:

<key>aps-environment</key>
<string>development</string>

Info.plist:

<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>

At least I have the following configuration in Info.plist ;-). Configuratioin of Entitlements.plist is described here. Please ensure that you do NOT startup the application with an attached debugger as this might cause issues (at the very least with Android and more specifically: stopping the application with the debugger, not attaching a debugger in itself). I would recommend sending data manually via a backend to do your testing (see my comment https://github.com/TobiasBuchholz/Plugin.Firebase/issues/145 for a test notification).

For reference my solution for Android 13: https://github.com/TobiasBuchholz/Plugin.Firebase/issues/145#issuecomment-1455182588. However I like yours better. I think I might integrate this into my application. Can you tell me where you are calling var status = await Microsoft.Maui.ApplicationModel.Permissions.CheckStatusAsync<Notification>(); from?

axylophon commented 1 year ago

Hey thanks for your reply!

Thank you for your insights. Its a shame that there is still no support from Microsoft in such a vital feature for mobile apps.

I have a prototype ready for Android but I am still struggling with the details. Foreground/Background/Icon etc. I also had some struggles with missing notifications which i cannot explain. No changes to the app, tried with or without debugging. Sometimes it works sometimes not.

Am I right that this package always displays a push notification even when the app is in foreground?

Next week I am looking to do the same for iOS but my dev Mac is right now in repair 😅

Do you maybe have an idea how i can implement the iOS permission handling manually? It would maybe be possible to inhert also from BasePlatformPermission/BasePermission. But i would need some guidance how to request/check the status.

Here my use case that you understand it better:

I have a Blazor MAUI Hybrid App. Maui is just a wrapper for the Blazor Application.

During the app login I have a specific page in the Blazor application which explains the user the notifications and asks if the user wants them. If accepted I trigger the permission request for Android. For this i inject an Interface that abstracts the Maui Permission API. The implementation of this Interface is done in the maui project. If the user denies it I show a warning and an additional explanation.

I want the same behaviour also for iOS to trigger the system permission dialog and react to the response as in Android 13+.

Thank you for your comment to my Android Permission handling. I think its a rather nice solution that could easily integrated directly into Maui.

andyzukunft commented 1 year ago

Yeah, there is a lot of features lacking in MAUI. It gets really bad if you use a Microsoft MAUI component (such as the Bottom Tab Bar), you want a feature but it is not there (like: I want four tabs at the most). In this case you are lost and have to to a lot of changes to your source code (in my case: removing Shell, replacing the Bottom Tab Bar with a custom tab bar etc. pp.). Actually at the moment I would recommend to use as few Microsoft components as possible if you want to do customization (layout, font style, font colors) than whatever is the default. But using MAUI Hybrid seems to indicate this is not a problem on your side.

Notifcation behaviour: The notification will always be displayed whether the App is in foreground, in background or closed. I THINK there are event hooks which let you either handle the message yourself (app is active) or not (app is active with special behaviour, app is inactive or closed) however at the moment I am satisfied with the default behaviour.

iOS: I tried to check my iOS app however at the moment I cannot build iOS because I have fcm related build copy errors due to long file names. Don't know when they appeared :-/. There is always a problem with MAUI. Especially with iOS.

I think the iOS app behaves the following: when configured to use the Notification Permission the system will ask the user at the moment he is launching the app. I don't think you can override this. Might be a "good" decision on MAUI part or it could be a limitation of iOS.

axylophon commented 1 year ago

Yeah with Blazor we have more freedom regarding the UI but it has also issues like the interaction between MAUI and Blazor and Webview issues in general.

Thanks i will take a look when my Mac is ready again.

Regarding the build error:

I also had a file copy build error when adding the FCM Nuget package although the file was clearly there. I currently removed the iOS target framework as i hoped that it is a windows issue...

Could you maybe post the error message?

We should try to find a workaround/fix for it or start an issue at microsoft to get this fixed...

andyzukunft commented 1 year ago

For your information even though it is off topic: I tried to create a bug report regarding their .NET 8 MAUI "build script" for an iOS build (.NET 8, so Visual Studio Beta) a couple of weeks ago which I had an workaround and spend 1 hour of collecting data for the report I had to supply something event more (don't remember what it was) which would have cost me hours of work most likely. Which is were I quit my attempt of reporting the bug.

When enabling iOS as Target Framework I get the following error (when building Android OR iOS):

Error   MSB3027 Could not copy "C:\nuget\xamarin.firebase.ios.core\8.10.0.3\lib\net6.0-ios15.4\Firebase.Core.resources\GoogleAPIClientForREST.xcframework\ios-arm64_x86_64-simulator\GoogleAPIClientForREST.framework\Headers\GoogleAPIClientForREST-umbrella.h" to "bin\Debug\net7.0-ios16.2\iossimulator-x64\Firebase.Core.resources\GoogleAPIClientForREST.xcframework\ios-arm64_x86_64-simulator\GoogleAPIClientForREST.framework\Headers\GoogleAPIClientForREST-umbrella.h". Exceeded retry count of 10. Failed.
Error   MSB3027 Could not copy "C:\nuget\xamarin.firebase.ios.installations\8.10.0.3\lib\net6.0-ios15.4\Firebase.Installations.resources\FirebaseInstallations.xcframework\ios-arm64_x86_64-simulator\FirebaseInstallations.framework\Headers\FirebaseInstallations-umbrella.h" to "bin\Debug\net7.0-ios16.2\iossimulator-x64\Firebase.Installations.resources\FirebaseInstallations.xcframework\ios-arm64_x86_64-simulator\FirebaseInstallations.framework\Headers\FirebaseInstallations-umbrella.h". Exceeded retry count of 10. Failed.
...

iOS enabled as Target Framework:

<TargetFrameworks>net7.0-android33.0;net7.0-ios16.2</TargetFrameworks>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">28.0</SupportedOSPlatformVersion>

I think its related to the file path length which exceeds 255 characters. But don't you worry. I think (and strongly hope) it was introduced with the current Visual Studio v17.6.4 and possibly it will be resolved with the next. My friend has the same issue (but don't know if this happened for him before the VS update ...). Truth is: the MAUI iOS build / deploy keeps on breaking like every second Visual Studio update.

axylophon commented 1 year ago

Thanks!

I got my Mac back from repair - the push notifications are working at the first glance on the latest iOS 🙂 🎉- I forgot an Initialize call that was mentioned in the readme but not in the sample as there is the whole Plugin.Firebase nuget package used and not only the Plugin.Firebase.CloudMessaging one 😂

I have the latest SDK, XCode, VS installed on MacOS. It is working and building for me in VS. But i cannot start it via Rider... 🙄

I also had the same issue with copying the file. It occured only on Windows when the iOS target framework was in the project file as soon as i installed the FCM nuget package. I adapted the csproj file that on Windows only Android and on MacOS only iOS is built.

Maybe that issue is helping? https://github.com/xamarin/xamarin-macios/issues/18445

I still got two open tasks on my side:

  1. Fix the Notification Icon for Google Pixel Devices (somehow there is a problem with the Color/Material You design as the icon is only white/grey an not visible. On a Samsung device the icon is working as expected.
  2. Handle the iOS notification permission request manually and integrate it via the Maui Permission API. I have already an idea and will test it on Monday. If I have a working solution I will post it here 🙂
andyzukunft commented 1 year ago
It occured only on Windows when the iOS target framework was in the project file as soon as i installed the FCM nuget package. I adapted the csproj file that on Windows only Android and on MacOS only iOS is built.

That's just so sad but yeah ... I guess the way to go.

Regarding you Icon issue: can you provide an example? I can compare it to our icon. Actually I have to ask my friend, he has a Pixel and I don't think we tested it with Material You Design activated.

axylophon commented 1 year ago

Yeah i will provide an icon example next week 👍🏻 I tested it with the Android emulator. But i have also a personal pixel which i will test before sending you the example 🙂

Then i will also have feedback regarding the permissions.

axylophon commented 1 year ago

This is my current solution to request the permission manually on iOS:

With this implementation i can use the MAUI API for requesting permissions and control the behaviour as i like.

https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/appmodel/permissions?tabs=macios#extending-permissions

#if ANDROID
await CrossFirebaseCloudMessaging.Current.CheckIfValidAsync();
#endif

This is the content of my Notification.cs file:

#if ANDROID
public class Notification : Microsoft.Maui.ApplicationModel.Permissions.BasePlatformPermission
{

    public override (string androidPermission, bool isRuntime)[] RequiredPermissions =>
        new List<(string androidPermission, bool isRuntime)>
        {
            (Android.Manifest.Permission.PostNotifications, true),
        }.ToArray();
}
#elif IOS
public class Notification : Microsoft.Maui.ApplicationModel.Permissions.BasePermission
{
    public override Task<PermissionStatus> CheckStatusAsync()
    {
        var taskCompletionSource = new TaskCompletionSource<PermissionStatus>();

        UNUserNotificationCenter.Current.GetNotificationSettings((settings) =>
        {
            var status =  settings.AuthorizationStatus switch
            {
                UNAuthorizationStatus.Authorized => PermissionStatus.Granted,
                UNAuthorizationStatus.Denied => PermissionStatus.Denied,
                UNAuthorizationStatus.Ephemeral => PermissionStatus.Granted,
                UNAuthorizationStatus.NotDetermined => PermissionStatus.Unknown,
                UNAuthorizationStatus.Provisional => PermissionStatus.Granted,
                _ => PermissionStatus.Unknown
            };

            taskCompletionSource.SetResult(status);
        });

        return taskCompletionSource.Task;
    }

    public override Task<PermissionStatus> RequestAsync()
    {
        var taskCompletionSource = new TaskCompletionSource<PermissionStatus>();
        UNUserNotificationCenter.Current.RequestAuthorization(
            UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,
            (granted, error) => {
                if(!granted) {
                    taskCompletionSource.SetResult(PermissionStatus.Denied);
                }
                else
                {
                    taskCompletionSource.SetResult(PermissionStatus.Granted);
                }
            });

        return taskCompletionSource.Task;
    }

    public override void EnsureDeclared()
    {
    }

    public override bool ShouldShowRationale()
    {
        return false;
    }
}
#endif
axylophon commented 1 year ago

@andyzukunft: regarding the icon issue:

I have put the svg icon under \Resources\Images\notify.svg. It is also the same file that i use for the app icon itself. It is this file:

notify

Images are included in the csproj file:

<!-- Images -->
<MauiImage Include="Resources\Images\*" />

I added it to the AndroidManifest.xml:

<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notify" />

And when creating the channel in MainActivity.cs:

FirebaseCloudMessagingImplementation.SmallIconRef = Resource.Drawable.notify;

Here are the results (Android 13):

Samsung: Icon looks good: icon_1_samsung

I think the white circle around the icon is too large here: icon2_samsung

Pixel:

Icon not visible (color issue?): icon1_pixel

Icon not visible (color issue?): icon2_pixel

angelru commented 1 year ago

@andyzukunft: con respecto al problema del icono:

He puesto el ícono svg en \Resources\Images\notify.svg . También es el mismo archivo que uso para el ícono de la aplicación. Es este archivo:

notificar

Las imágenes están incluidas en el archivo csproj:

<!-- Images -->
<MauiImage Include="Resources\Images\*" />

Lo agregué a laAndroidManifest.xml:

<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notify" />

Y al crear el canal en MainActivity.cs:

FirebaseCloudMessagingImplementation.SmallIconRef = Resource.Drawable.notify;

Aquí están los resultados (Android 13):

Samsung: El icono se ve bien: icono_1_samsung

Creo que el círculo blanco alrededor del icono es demasiado grande aquí: icono2_samsung

Píxel:

Icono no visible (¿problema de color?): icono1_píxel

Icono no visible (¿problema de color?): icono2_pixel

In android it is recommended to have an icon with a white background and the rest of the icon in black, within the resources of the Android project.

axylophon commented 1 year ago

I created a new Android icon which is working fine now :) We are now in a test phase before going live - so far it looks good. But i observered another problem - i will create a new issue for it.

leroygumede commented 9 months ago
It occured only on Windows when the iOS target framework was in the project file as soon as i installed the FCM nuget package. I adapted the csproj file that on Windows only Android and on MacOS only iOS is built.

That's just so sad but yeah ... I guess the way to go.

Any chance you can share sample code ?

axylophon commented 9 months ago

Any chance you can share sample code ?

This is how I have done it right now in the csproj file:

<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('osx'))">$(TargetFrameworks);net7.0-ios</TargetFrameworks><TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0-android33.0</TargetFrameworks>

andyzukunft commented 1 month ago

@axylophon I want to backtrack some of this topic. You mentioned and provided an example of how you circumvent the permission request for iOS during application startup. Is this still your production code?

Around two months ago Microsoft implement the Post Notification permission into MAUI and I am wondering if any special implementation is required or if we still need special behaviour for iOS.

axylophon commented 1 month ago

Hi,

yes it is still my production code. As far as i know the permission was only implemented for Android and not for iOS?

So I believe for iOS the special handling is still necessary if more control is needed when the permission popup should be visible to the user.

andyzukunft commented 1 month ago

It seems you might be correct. I am currently adapting your iOS example to work with my app.

Can you please verify something for me? I am only calling CheckIfValidAsync() for Android however the iOS dialog "App Would Like to Send You Notifications" still pops up on startup. Can you confirm this does (not) appear for you on a "clean" emulator (e.g. where the app has been deinstalled beforehand)? It seems FirebaseCloudMessagingImplementation.Initialize() implemented in MauiProgram.cs also checks and requests the permision. How do you handle that?

// Example for MAUI App.xaml.cs OnStart()
protected override void OnStart()
{
#if ANDROID
    CrossFirebaseCloudMessaging.Current.CheckIfValidAsync().Wait();
#endif
}
// Example for MAUI MauiProgram.cs Firebase initialization
private static MauiAppBuilder RegisterFirebaseServices(this MauiAppBuilder builder)
{
#if ANDROID
    // Init Android components
#elif IOS
    builder.ConfigureLifecycleEvents(events =>
    {
        events.AddiOS(iOS => iOS.WillFinishLaunching((_,__) => {
            CrossFirebase.Initialize();
            FirebaseCloudMessagingImplementation.Initialize();

        return false;
    }
#endif
}

Update: It also happens if I remove FirebaseCloudMessagingImplementation.Initialize():

// Example for MAUI MauiProgram.cs Firebase initialization
private static MauiAppBuilder RegisterFirebaseServices(this MauiAppBuilder builder)
{
#if ANDROID
    // Init Android components
#elif IOS
    builder.ConfigureLifecycleEvents(events =>
    {
        events.AddiOS(iOS => iOS.WillFinishLaunching((_,__) => {
            CrossFirebase.Initialize();

        return false;
    }
#endif
}
axylophon commented 1 month ago

FYI: I am still on version 2.0.4. I only use the Plugin.Firebase.CloudMessaging nuget package.

Yes with my solution I never saw the iOS permission dialog on app start. I could trigger it myself. Last time I tested it was iOS 15. If you cannot get it to run I could test it again on my device if anything changed.

Maybe it has something to do with the 3.0 version and the iOS dependency updates? Have you tried the older version 2.0.4?

Here are my code snippets:

MauiProgram.cs:

    private static MauiAppBuilder RegisterFirebaseServices(this MauiAppBuilder builder)
    {
        builder.ConfigureLifecycleEvents(events =>
        {
#if IOS
            events.AddiOS(iOS => iOS.WillFinishLaunching((_, launchOptions) => {
                CrossFirebase.Initialize();
                FirebaseCloudMessagingImplementation.Initialize();
                return false;
            }));
#else
            events.AddAndroid(android => android.OnCreate((activity, _) =>
                CrossFirebase.Initialize(activity)));
#endif
        });

        return builder;
    }

When getting the push token:


#if ANDROID
            await CrossFirebaseCloudMessaging.Current.CheckIfValidAsync(); //run check valid only on android as this triggers the permission request on iOS
#endif

            var token = await CrossFirebaseCloudMessaging.Current.GetTokenAsync();
axylophon commented 1 month ago

@andyzukunft : I just read about all the iOS binding issues and the version 3.0.0. I was not actively developing the app in the last months. If i understand it right i should update ASAP to 3.0.0.

Should i expect any issues with 3.0.0? What is your experience?

andyzukunft commented 1 month ago

I gave my fcm implementation some love within the last week. One part of it was updating the Plugin.Firebase.CloudMessaging to v3.0.0. As far as I can tell the update works with the usual Visual Studio troubles (-> long file names). The running app is working without problems as far as I can tell. Well there might be the permission dialog - but I hope it's not related.

The Visual Studio workaround is the following:

Later on, when you want to build a release version I believe you have to use the terminal to create an initial build as well. I am not that impacted by that because I started to use the terminal to publish release builds anyway.

It would be great if you can check on your end if the permission dialog appears for v2.0.4 and later on for 3.0.0. I think I will go with my current solution (based on yours) for the moment. While it does not properly asks for the iOS permission where I want it, it does can check it at the point I want it to and prevent the user going further without having notifications activated.

axylophon commented 1 month ago

I have put the 3.0.0 update on my agenda now. Have you already published a new iOS version with 3.0.0? Any problems with the app store?

Do you know if the version 2.0.4 and its dependencies use the legacy FCM API? If yes the push notifications for iOS users are maybe already broken. I have no access to an iOS device right now to test it.

I will give you some feedback once i have updated to 3.0.0.

andyzukunft commented 1 month ago

I don't know what kind of API is being used. But it sounds reasonable that the old FCM API was being used. However I believe it was still working with iOS.

I already pushed my app (MAUI v8.0.71, Plugin.Firebase.CloudMessaging v3.0.0) to the iOS store and it is running in production for a few days. So far I don't see any problems.

axylophon commented 1 month ago

That sounds nice thank you!

Yeah maybe I am confused with the APIs. I am not sure if this is even relevant for the client.