dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
21.82k stars 1.66k forks source link

iOS Background Update Notification is starting the app #13822

Open tranb3r opened 1 year ago

tranb3r commented 1 year ago

Description

When a MAUI iOS app receives a Background Update Notification, DidReceiveRemoteNotification is called, in order to process a background update. If the app was previously killed, then MAUI creates the app (which is fine) but it also starts the app (which is not fine). On Android, in a similar scenario, the app is created, but not started. Could you please fix it?

More details about the succession of events:

  1. MauiUIApplicationDelegate creates MauiApp
  2. Maui Application constructor is called
  3. Maui Application OnStart is called -> this should not happen
  4. MauiUIApplicationDelegate DidReceiveRemoteNotification is executed

In my app, the bad consequence is user being automatically logged out when the app receives a Background Update, because starting the app triggers a biometric check, and as it fails, the user is signed out.

Steps to Reproduce

  1. Create a MAUI app
  2. Implement DidReceiveRemoteNotification for background update notification
  3. Launch the app
  4. Kill the app
  5. Send a background update notification

Link to public reproduction project repository

no need

Version with bug

7.0 (current)

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iOS 16.2

Did you find any workaround?

no workaround

Relevant log output

No response

ghost commented 1 year ago

Hi @tranb3r. We have added the "s/needs-repro" label to this issue, which indicates that we require steps and sample code to reproduce the issue before we can take further action. Please try to create a minimal sample project/solution or code samples which reproduce the issue, ideally as a GitHub repo that we can clone. See more details about creating repros here: https://github.com/dotnet/maui/blob/main/.github/repro.md

This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

tranb3r commented 1 year ago

Simply add this code to your ios app. Then create and send a POST Request to APNs (sandbox) in order to trigger a background update.

    [Register("AppDelegate")]
    internal class AppDelegate : MauiUIApplicationDelegate
    {
        protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

        [Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")]
        public void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action<UIBackgroundFetchResult> completionHandler)
        {
             Console.WriteLine("DidReceiveRemoteNotification");
        }
    }
drasticactions commented 1 year ago

What happens if you create an iOS app without MAUI (dotnet new ios) and do the same thing?

tranb3r commented 1 year ago

What happens if you create an iOS app without MAUI (dotnet new ios) and do the same thing?

I don't know. How is this relevent, since the issue is about the maui app starting (instead of simply being created)? Assuming I succeed in running an iOS app without maui (which I have never done), what should I look for?

drasticactions commented 1 year ago

A MAUI iOS App is a .NET iOS app with additional tooling.

https://github.com/dotnet/maui/blob/main/src/Core/src/Platform/iOS/MauiUIApplicationDelegate.cs#L26-L29

This is where CreateMauiApp is called. willFinishLaunchingWithOptions is, I'm assuming, called whenever you run that POST to start the background process. If that's the case, you can repro that (willFinishLaunchingWithOptions invoking, followed by didReceiveRemoteNotification) with a straight iOS app without MAUI. If that's true, then willFinishLaunchingWithOptions always invoke.

Going by this comment (https://github.com/dotnet/maui/commit/53a1d32d9114893a4a92dec727e6a2e22b6305e2) we've always called on the app startup within that method (The method names and the underlying code are different, but the idea is the same, the application starts within that method). The method parameters, at first glance, don't seem to differentiate how the application starts, just that it did.

That said, you could override WillFinishLaunching and handle that yourself.

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

    [Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")]
    public void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action<UIBackgroundFetchResult> completionHandler)
    {
        Console.WriteLine("DidReceiveRemoteNotification");
    }

    public override bool WillFinishLaunching(UIApplication application, NSDictionary launchOptions)
    {
        // Add your code here.

        // This will call the underlying MauiUIApplicationDelegate code, calling CreateMauiApp.
        return base.WillFinishLaunching(application, launchOptions);
    }
}
tranb3r commented 1 year ago

@drasticactions I can override WillFinishLaunching, but what for? You've explained that CreateMauiApp is called in WillFinishLaunching, and this is totally fine. However, why is OnStart being called as well? This is happening after WillFinishLaunching is done.

drasticactions commented 1 year ago

You mean this? https://github.com/dotnet/maui/blob/d0490abd10a9197aa2293a74da539dc28aea0ab7/src/Controls/src/Core/Application.cs#L316-L318

That gets called from https://github.com/dotnet/maui/blob/main/src/Core/src/Platform/iOS/MauiUIApplicationDelegate.cs#L52, when the Window (and by extension, the Application) is being created.

Because the application is being created and started, hence why Application.OnStart would be called. WillFinishLaunching is first, followed by FinishedLaunching.

If you override WillFinishLaunching, you can shortcircuit this by handling it and not calling CreateMauiApp and, by extension, not create the Platform Window and create the MAUI Application at all.

tranb3r commented 1 year ago

Well, I do need to create the application. That's where all the DI stuff is initialized. I can't process the background update if there is no app. But I don't want the app to start. It does not make sense to start it, since the UI is actually not visible in this scenario.

So, I can override WillFinishLaunching, but how do I know that I'm currently processing a background update and not starting the app? Also, why is this behavior different from what is done on Android, where the app is created, but not started?

mattleibow commented 1 year ago

I am not an iOS lifecycle expert, but I believe the order of operations is:

In this case, you can override didReceiveRemoteNotification to set some sort of state on the application. However, the same data passed in at the userInfo is also present in willFinishLaunchingWithOptions in the launchOptions argument, but under the UIApplication.LaunchOptionsRemoteNotificationKey key.

If you want the services all set up, then you can override WillFinishLaunching and call base. I am not sure how you tell iOS that you don't want to launch the app, maybe returning false? I am not sure.

    public override bool WillFinishLaunching(UIApplication application, NSDictionary launchOptions)
    {
        // This will call the underlying MauiUIApplicationDelegate code, calling CreateMauiApp.
        base.WillFinishLaunching(application, launchOptions);

        // Add your code here.

        return false; // ? maybe this?
    }
tranb3r commented 1 year ago

@mattleibow That is not correct. The order of operations is:

tranb3r commented 1 year ago

Any idea for fixing this issue?

mattleibow commented 1 year ago

Does the launch options not contain the notification information?

I saw in the docs that the data is present in willFinishLaunchingWithOptions in the launchOptions argument, but under the UIApplication.LaunchOptionsRemoteNotificationKey key.

ghost commented 1 year ago

Hi @tranb3r. We have added the "s/needs-info" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

tranb3r commented 1 year ago

Yes, the launchOptions does contain the notification information under the UIApplication.LaunchOptionsRemoteNotificationKey key. But the return value in WillFinishLaunching or FinishedLaunching is ignored when receiving a remote notification.

So, in WillFinishLaunching, there is nothing I can do to prevent FinishedLaunching from being called.

In FinishedLaunching (which is where App OnStart is being called), I can skip calling base.FinishedLaunching, and it actually prevents the app from being launched, which is what I want. I can then process DidReceiveRemoteNotification with services properly initialized.

However, when after that I launch the app manually, a black screen appears and the app is not displayed. What is strange is that the app log shows that the app is starting normally. But only a black screen is shown. Any idea what's happening?

homeyf commented 7 months ago

Verified this issue with Visual Studio Enterprise 17.9.0 Preview 1.0. Can repro on iOS platform.

ghost commented 7 months ago

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

mattleibow commented 7 months ago

@Redth you are our notifications expert!