Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support deep linking into .NET MAUI Blazor apps #3788

Closed
Tracked by #14022
danroth27 opened this issue Dec 16, 2021 · 43 comments
Closed
Tracked by #14022

Support deep linking into .NET MAUI Blazor apps #3788

danroth27 opened this issue Dec 16, 2021 · 43 comments
Labels
area-blazor Blazor Hybrid / Desktop, BlazorWebView Cost:M Work that requires one engineer up to 2 weeks discussed Created by mkArtakMSFT to help with planning temporarily. It will be removed after planning is done. Priority:1 Created by mkArtakMSFT proposal/open t/docs 📝 User Story A single user-facing feature. Can be grouped under an epic.

Comments

@danroth27
Copy link
Member

Description

Investigate supporting deep linking into pages in a .NET MAUI Blazor app

Public API Changes

TBD

Intended Use-Case

Use a link to navigate to a page in a .NET MAUI Blazor app

@Eilon Eilon added the area-blazor Blazor Hybrid / Desktop, BlazorWebView label Dec 16, 2021
@PureWeen
Copy link
Member

PureWeen commented Dec 16, 2021

@Eilon I know Shell navigation was added to .NET MAui Blazor Apps

Does that work provide deep linking into .NET MAUI Blazor Apps if you are using Shell?

PR for reference
dotnet/MobileBlazorBindings#152

@Eilon
Copy link
Member

Eilon commented Dec 16, 2021

@PureWeen we might need to build something similar, but not likely quite the same. I think that feature in Mobile Blazor Bindings (MBB) was for native UI built using Blazor (in MBB we supported true native UI using Xamarin.Forms w/ Blazor syntax, and also using web UI in a BlazorWebView). I don't recall us doing anything for deep-linking into web UI, but it might contain some similarities.

@PureWeen
Copy link
Member

@Eilon makes sense!

The routing parts of Shell are all very custom built so also seeing if there's anything here we can reuse/leverage as well
But the overlap might be very minimal

@mkArtakMSFT mkArtakMSFT added this to the .NET 7 milestone Mar 3, 2022
@mkArtakMSFT mkArtakMSFT modified the milestones: .NET 7, Future May 4, 2022
@ghost
Copy link

ghost commented May 4, 2022

We've moved this issue to the Future milestone. This means that it is not going to be worked on for the coming release. We will reassess the issue following the current release and consider this item at that time.

@Ghevi
Copy link

Ghevi commented Jul 5, 2022

Hi, excuse me if this is not the right place to ask.
Since deep linking is not yet supported out of the box in Maui, I tried implementing it in the native code, for example with android, in the MainActivity.cs file. It works and I managed to pass parameters and access them with intent.dataString.
In order to access this data in the razor components, I save it in MainActivity.cs with the Preferences api and then get it where I needed. This works but it's a bit cumbersome. The messaging center can't be use yet because razor components cannot registers callbacks for these messages at this stage, right?
I tried this protected override void OnAppLinkRequestReceived(Uri uri) in App.xaml.cs file but it doesn't seems to be called unless I made some mistake.
Any other solution?
If you want me I will move this to the discussions section.

@Eilon
Copy link
Member

Eilon commented Jul 5, 2022

@Ghevi I've never played with deep linking, but in general the strategy would be something like this:

  1. Hook into however each platform does its deep linking and get the requested link
  2. If the link goes to content that is in a BlazorWebView, then do the necessary MAUI actions to make sure that BlazorWebView is visible and active
  3. Once the BlazorWebView is ready and loaded, use Blazor's navigation APIs to go to the exact content that was requested

Unfortunately I don't know anything about part (1), but I would say it's highly likely that it's either exactly the same or almost exactly the same as how to do it in Xamarin.Forms apps, so perhaps there are good docs for Xamarin.Forms that could shed some light on it?

@Ghevi
Copy link

Ghevi commented Jul 6, 2022

@Eilon I managed to do part (1), it was nice to learn how to make it work btw.
Unfortunately I'm a bit lost on part (2). In Xamarin I found that there was a method called LoadApplication(myString) that you can call in MainActivity.cs hooks, but there isn't in Maui right?
Basically I'm not sure how to pass a piece of data from platform specific code, in this case MainActivity.cs to for example a Razor Component with the @page tag or even to the App.xaml.cs. I mean I managed to do it with the Preferences api but again I feel like it's not the best solution.

Also thank You for the help!

@Eilon
Copy link
Member

Eilon commented Jul 6, 2022

@Ghevi this is well outside my expertise, but I think for (2) it depends on what your app's UI looks like. For example, if your app is based on the .NET MAUI Shell control, then I think you would tell the Shell control to navigate to the relevant part of the URL, and you can read more about that here: https://docs.microsoft.com/dotnet/maui/fundamentals/shell/navigation. And if that page happens to contain a BlazorWebView, you could wait until it's done loading and send it any additional navigation information.

For example, let's say the URL is /myApp/users/info/12345. The myApp/users page is a Shell page, so you would take that part of the URL and tell Shell to navigate there. But what about the info/12345? Let's say that page within Shell uses a BlazorWebView, so once that view is done loading, you would pass that info/12345 to the Blazor navigation system to make sure the right user info page is shown.

@Ghevi
Copy link

Ghevi commented Jul 7, 2022

@Eilon I've read about the Shell and how to use it. But in working on a MAUI Blazor project, so there is no Shell unless I'm not aware of a way to use it in such case. Will do some more research, thank you for the help!

@Eilon
Copy link
Member

Eilon commented Jul 11, 2022

@Ghevi the Shell was just an example, so it depends on how your app is designed. The point in (2) is that if the deep link goes to a page in your app that is built with BlazorWebView, but that page isn't visible, you'll need to first make that page visible, and then do whatever Blazor navigation is needed.

@Ghevi
Copy link

Ghevi commented Jul 14, 2022

@Eilon yes, thanks i've understood what you ment. I improved my solution by using a service instead of the Preferences api, so i have more control over it. Before navigating to the index component, i check if there is any deep link data in my service and in that case use _navigationManager.NavigateTo("/deepLinkPage");.
At this point the BlazorWebView is visible so it works. Thank you for the suggestions :)

@MikeAndrews90
Copy link

MikeAndrews90 commented Oct 31, 2022

Ok after a bit more debugging I got it working in a similar way, but from a separate custom Activity instead of in MainActivity.

I have a custom Activity which is invoked when a particular URL is opened with the app. e.g https://myapp/go-here

I created a static NavigationService type class, where I can set a string which is the path taken from the incoming URL from the activity. In NavigationService I also added an event handler that I fire when the page is set, so it can be subscribed to somewhere in Blazor (I did mine in Main.razor) and then you can use the NavigationManager to navigate to the page.

From the Activity I just set NavigationService.Path = "go-here";

That looks like this:

protected override void OnNewIntent(Intent intent)
        {
            base.OnNewIntent(intent);

            var data = intent.DataString;

            if (intent.Action != Intent.ActionView) return;
            if (string.IsNullOrWhiteSpace(data)) return;

            var path = data.Replace(@"https://myapp", "");
            NavigationService.SetPage(path);
        }

In Main.razor I added a code block which just checks the NavigationService to see if there's a path set, and if so, it uses the standard NavigationManager to navigate to the path just set from the Activity.

That looks like this:

@code
{
    [Inject]
    public NavigationManager NavigationManager { get; set; }

    private static bool _subscribedToNavigationPageSetEvent;
    protected override void OnInitialized()
    {
        if (!_subscribedToNavigationPageSetEvent)
        {
            NavigationService.PageSet += NavigationServiceOnPageSet;
            _subscribedToNavigationPageSetEvent = true;
        }
    }

    private void NavigationServiceOnPageSet(object sender, object e)
    {
        if (!string.IsNullOrWhiteSpace(NavigationService.Page))
        {
            Debug.WriteLine(NavigationService.Page);
            NavigationManager.NavigateTo(NavigationService.Page, forceLoad:true);
            NavigationService.SetPage("");
        }
    }
}

And for convenience, here's my NavigationService code:

public static class NavigationService 
    {
        public delegate void PageSetEventHandler(object sender, object e);

        public static event PageSetEventHandler PageSet;

        public static string Page { get; private set; }

        public static void SetPage(string url)
        {
            Page = url;
            if (!string.IsNullOrEmpty(url))
            {
                PageSet?.Invoke(null, url);
            }
        }
    }

Not ideal, but it works!

@mkArtakMSFT mkArtakMSFT modified the milestones: Backlog, .NET 8 Planning Nov 3, 2022
@mkArtakMSFT mkArtakMSFT added the discussed Created by mkArtakMSFT to help with planning temporarily. It will be removed after planning is done. label Nov 3, 2022
@GolfJimB
Copy link

GolfJimB commented Nov 12, 2022

@MikeAndrews90 could you share your NavigationService code please. Also, how to do deep linking for ios?

@MikeAndrews90
Copy link

@GolfJimB It doesn't do much, just sets the current page path

    public interface INavService
    {
        public string Page { get; }
        void SetPage(string url);
    }

    public static class NavigationService 
    {
        public static string Page { get; private set; }

        public static void SetPage(string url)
        {
            Page = url;
        }
    }

I'm not targeting iOS so I don't know how to do deep linking on iOS.

@Ghevi
Copy link

Ghevi commented Nov 15, 2022

For iOS what worked for me was adding this method to the AppDelegate.cs class:

[Export("application:openURL:options:")]
   public override Boolean OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
   {
       var deepLinkService = DependencyService.Get<DeepLinkService>();
       deepLinkService.DeepLinkDto = null;
       if (!String.IsNullOrEmpty(url.AbsoluteString) && url.AbsoluteString.Contains("myapp"))
       {
           if (url.AbsoluteString.Contains("/resources-page"))
           {
               var resourceId= NSUrlComponents
                   .FromString(url.Query)?.QueryItems?
                   .Single(x => x.Name == "resourceId").Value;
               deepLinkService.DeepLinkDto = new DeepLinkDto(resourceId);
           }
       }

       return true;
 }

then add in the Entitlements.plist the options Associated Domains with a string applinks:www.myappurl.com and also in the Identifiers section of https://developer.apple.com/account/resources/identifiers/list the AssociatedDomains to the capabilities.
Keep in mind that if you want the app to open immediately without user prompt you have to go host an assetlinks.json file in one of your websites for android

@mkArtakMSFT mkArtakMSFT added Priority:1 Created by mkArtakMSFT discussed Created by mkArtakMSFT to help with planning temporarily. It will be removed after planning is done. and removed discussed Created by mkArtakMSFT to help with planning temporarily. It will be removed after planning is done. labels Nov 16, 2022
@mkArtakMSFT mkArtakMSFT added the t/enhancement ☀️ New feature or request label Dec 2, 2022
@GolfJimB
Copy link

@Ghevi what's the DeepLinkService and DeepLinkDto?

@Ghevi
Copy link

Ghevi commented Dec 13, 2022

@GolfJimB two classes i've made. The service contains the dto and has some other methods.

@mkArtakMSFT mkArtakMSFT removed this from the .NET 8 Planning milestone Feb 1, 2023
@davidortinau davidortinau added User Story A single user-facing feature. Can be grouped under an epic. Cost:M Work that requires one engineer up to 2 weeks labels Mar 17, 2023
@MikeAndrews90
Copy link

For anyone coming to this later, my comment above with my solution still works, however I've just edited it with something important. Previously, I was running StartActivity(typeof(MainActivity)) - However I have just discovered today that if you keep doing that, the app gets extremely laggy, I suspect its actually running the whole app/Blazor on top of the existing one every time its invoked, which obviously will be using a load of resources. I found a workaround, and that's by using an event handler, rather than executing StartActivity.

@tpmccrary
Copy link

@MikeAndrews90 Can you share a code snippet of using an event handler instead of StartActivity? Thanks in advance!

@MikeAndrews90
Copy link

MikeAndrews90 commented Mar 27, 2023

@tpmccrary I updated my comment above with it all in #3788 (comment)

@tpmccrary
Copy link

@MikeAndrews90 Thanks for updating it!

One other question. I noticed when the app is opened, deep linking works perfect with your approach. However, if the app is fully closed, deep linking does not work. It just sends me to the initial app page.
Have you experienced this?

@MikeAndrews90
Copy link

Don't think I have... Have you done an all the assetlinks.json stuff?

@tpmccrary
Copy link

@MikeAndrews90 I have not. What do I have to do to get that setup? Thanks for all the help by the way!

@MikeAndrews90
Copy link

You need an assetlinks.json file at the domain you're using for deep linking, e.g https://myapp.com/.well-known/assetlinks.json

A quick Google found an example in this answer on stack overflow:
https://stackoverflow.com/questions/48056006/how-to-create-assetlinks-json-when-website-is-handled-by-multiple-apps

To get your sha256 fingerprint for the json file follow Microsoft's docs https://learn.microsoft.com/en-us/xamarin/android/deploy-test/signing/keystore-signature?tabs=windows

@tpmccrary
Copy link

@MikeAndrews90 Would you be willing to share the contents of the Activity and IntentFilter attributes for both your MainActivity.cs and the other Activity.cs you created?

@MikeAndrews90
Copy link

@tpmccrary Here's my UrlActivity for the deeplinking: (The casing is a bit weird because I was in a rush to get it working, and havne't got round to sorting it, and it has to match in the manifest XML)

namespace myapp.app
{
    [IntentFilter(new[] { Intent.ActionView },
            Categories = new[]
            {
                Intent.CategoryBrowsable
            },
            DataScheme = "https",
            AutoVerify = true,
            DataHost = "myapp.com", 
            DataMimeType = "com.yourcompany.yourapp"            
        )
    ]
    [Activity(Name = "myapp.app.urlactivity", 
        MainLauncher = false, 
        Theme = "@style/Maui.SplashTheme", 
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    public class urlactivity : Activity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            SetTheme(Resource.Style.AppTheme);
            base.OnCreate(savedInstanceState);
            OnNewIntent(Intent);
        }

        protected override void OnNewIntent(Intent intent)
        {
            base.OnNewIntent(intent);

            var data = intent.DataString;

            if (intent.Action != Intent.ActionView) return;
            if (string.IsNullOrWhiteSpace(data)) return;

            var path = data.Replace(@"https://myapp.com", "");
            NavigationService.SetPage(path);
        }
    }

And MainActivity:

namespace MyApp.App
{
    [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    [IntentFilter(new[] { NfcAdapter.ActionNdefDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/com.yourcompany.yourapp")]
    public class MainActivity : MauiAppCompatActivity
    {    
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
        }

        protected override void OnResume()
        {         
            base.OnResume();
        }

        protected override void OnNewIntent(Intent intent)
        {
            base.OnNewIntent(intent);      
        }

        // app minimise
        protected override void OnPause()
        {
            ConnectivityStatus.Stop();
            base.OnPause();
        }

        // app close
        protected override void OnDestroy()
        {
            base.OnDestroy();
        }
    }
}

And don't forget to add it into your AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
	<application android:allowBackup="true" tools:replace="android:allowBackup,android:label" android:icon="@mipmap/icon_background" android:roundIcon="@mipmap/icon_background" android:supportsRtl="true">
		<activity android:name="myapp.app.urlactivity" android:exported="true">
			<intent-filter android:autoVerify="true">
				<action android:name="android.intent.action.VIEW" />
				<category android:name="android.intent.category.DEFAULT" />
				<category android:name="android.intent.category.BROWSABLE" />
				<data android:scheme="https" android:host="myapp.com" />
			</intent-filter>
		</activity>
	</application>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.NFC" />
	<uses-feature android:name="android.hardware.nfc" android:required="false" />
	<queries>
		<intent>
			<action android:name="android.support.customtabs.action.CustomTabsService" />
		</intent>
	</queries>
</manifest>

@MarcoErlwein
Copy link

Could you please share the project, to see the complete code?
Thanks !!!

@SamVanhoutte
Copy link

All of this just does not seem to work for me.
In Android, I can manage to trigger the CheckForAppLink method, as described above, and I can see the NavigationManager.NavigateTo() method being called , but it does not move the Blazor page to that uri.

It would be really very helpful to see a project, that works end-to-end with the above approaches.

@gerneio
Copy link

gerneio commented May 17, 2023

Just to share my experiences, for Android specifically (still need to get around to iOS), few things that helped me, in addition to all the code shared from above:

  1. Default MAUI template does not specify android:launchMode for the MainActivity, therefore the default will be set to standard (see here). Therefore, if an intent is called into your app, even if your app/activity has already been created, it will, by default, always creates a new instance of the activity. So this explains why OnNewIntent was never called directly. The quick fix (as shown above) was to just have OnCreate always call OnNewIntent, but a better fix IMO is to set the launch mode to something like singleTop. This would also prevent your activity from being re-created (perf benefit?), which, for me at least, was preventing the app from visually re-navigating (might have been related to my use of the NavigationPage control as my MainPage) and now calling OnNewIntent directly. It's still a good idea to have the OnCreate call OnNewIntent, so we can handle Intent's that also triggered the app to be started. Overall, my MainActivity looks something like this:
// Will both automatically be written to the generated `AndroidManifest.xml` file on build (check `..\obj\Debug\net7.0-android\AndroidManifest.xml`)
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, 
    AutoVerify = true, DataScheme = "https", DataHost = "mysite.com")]
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, 
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density,
    // Prevents MainActivitiy from being re-created on launching an intent (also makes it to where `OnNewIntent` will be called directly, if the app has already been loaded)
    LaunchMode = LaunchMode.SingleTop 
)]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        // In case the app was opened (on first load) with an `ActionView` intent
        OnNewIntent(this.Intent); 
    }

    protected override void OnNewIntent(Intent intent)
    {
        base.OnNewIntent(intent);

        var data = intent.DataString;

        if (intent.Action != Intent.ActionView) return;
        if (string.IsNullOrWhiteSpace(data)) return;

        // TODO: Call some sort of propogation service to pass down `data`, for example:
        var appServices = ServiceHelper.Current.GetRequiredService<AppServices>(); // MAUI cross-platform service resolver: https://stackoverflow.com/a/73521158/10388359
        appServices.OnAppLinkReceived(data);
    }
}

public class AppLinkReceivedEventArgs : EventArgs
{
    public required string Data { get; set; }
}

// Don't forget to register this in DI as a singleton
public class AppServices
{
    public event EventHandler<AppLinkReceivedEventArgs>? AppLinkReceived;
    public string? LastAppLink { get; private set; }

    public void OnAppLinkReceived(string data)
    {
        LastAppLink = data;

        AppLinkReceived?.Invoke(this, new() { Data = data });
    }
}
  1. Realizing that we don't really need to update AndroidManifest.xml directly for this since the IntentFilterAttribute will basically be written directly to it at build time. You can evaluate the generated file by looking here: ..\obj\Debug\net7.0-android\AndroidManifest.xml

  2. Don't forget to setup the assetlinks.json file at the root of your domain (i.e. https://mysite.com/.well-known/assetlinks.json). I cheated a little and got the sha256_cert_fingerprints from what was already built against my MAUI app and delivered to the android emulator. From VS, start Tools > Android > Android ADB Command Prompt. Make sure the emulator is already started and that you deployed a version of the app with the IntentFilter filled out. Run the cmd adb shell pm get-app-links com.companyname.myapp and copy the value shown in Signatures to your assetlinks.json file. Re-deploy the app and run the get-app-links command again after a little bit to see if the domain was verified. Not sure if this will hold through deploying to the official app store, so guess I'll find out later once I get there.

Some other useful ADB commands for debugging android deep links (see also):

# Resets verified app link states for your app
adb shell pm set-app-links --package com.companyname.myapp 0 all

# Manually trigger verification for your app (will be auto triggered with `android:autoVerify="true"`)
adb shell pm verify-app-links --re-verify com.companyname.myapp

# Manually trigger an `action.VIEW` intent
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://mysite.com/some/path?q=1"
  1. Now depending on how you decide to store and propagate this intent event, on the Blazor side (and probably same for a MAUI page), you'll need to listen for those events. In my case, I opted for registering an AppServices class in DI as a singleton. My Blazor component that is responsible for handling these app links must then subscribe to the AppLinkReceived event and then act on it accordingly. Biggest pain here is when the Intent is also what starts up your app in the first place, therefore there is no guarantee that your BlazorWebView has finished initializing, and same for your Blazor component of course. To solve this, I basically just store the last intent data string in AppServices.LastAppLink. So my Blazor component will now also check to see if LastAppLink is filled in, and if so then go ahead and process it accordingly. Ideally, this would also mean that you should put this logic in a component that is Initialized only once. Something like this:
@implements IDisposable
@inject AppServices appServices

@code {
    protected override void OnInitialized()
    {
        appServices.AppLinkReceived += AppServices_AppLinkReceived;

        if (!string.IsNullOrEmpty(appServices.LastAppLink))
        {
            AppServices_AppLinkReceived(null, new() { Data = appServices.LastAppLink });
        }
    }

    private void AppServices_AppLinkReceived(object? sender, AppLinkReceivedEventArgs e)
    {
        // TODO: Parse `e.Data` and determine how to route it
    }
 
    void IDisposable.Dispose()
    {
        appServices.AppLinkReceived -= AppServices_AppLinkReceived;
    }
}

Note that in my use case I didn't need to actually Navigate to any page, so I'm not currently using the NavigationManager, so your mileage may vary.

If anyone ever gets to a point of putting together a full end-to-end example, just an idea, but I think it might be possible to use Github pages to host the assetlinks.json (ref here) so that we can have a fully working example that anyone can just run and quickly experiment with, w/o additional setup on their part. If I have the same sort of trouble setting up iOS (once I get to it), then I might take the initiative (no promises!).

@SamVanhoutte
Copy link

@gerneio , thanks for your sample.
the key missing part for me, is where I can and should intercept the event (AppLinkReceived) in the BlazorSetup , in order to navigate to the right location. That is (probably), the only real missing part for now, for me.

@gerneio
Copy link

gerneio commented Jun 2, 2023

@SamVanhoutte I would put this in your top most common layer that's shared across all your pages. So maybe MainLayout.razor or Main.razor. The razor code I shared above is pretty much exactly what u need, with a few modifications to the event handler AppServices_AppLinkReceived. You should then use the built in NavigationManager.NavigateTo to navigate to the specific route you're needing. Something like this perhaps:

MainLayout.razor

@inherits LayoutComponentBase
@implements IDisposable
@inject AppServices appServices
@inject NavigationManager nav

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

@code {
    protected override void OnInitialized()
    {
        appServices.AppLinkReceived += AppServices_AppLinkReceived;

        if (!string.IsNullOrEmpty(appServices.LastAppLink))
        {
            AppServices_AppLinkReceived(null, new() { Data = appServices.LastAppLink });
        }
    }

    private void AppServices_AppLinkReceived(object? sender, AppLinkReceivedEventArgs e)
    {
        // Assuming e.Data represents a URL value
        // Examples: `https://myhost.com/counter?initCount=12345` or `myapp://mobile/counter?initCount=12345`
        if (Uri.TryCreate(e.Data, UriKind.Absolute, out var uri))
        {
            var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);

            // TODO: Do some additional checking to make sure URL is in expected format
            if (path.Equals("counter", StringComparison.OrdinalIgnoreCase))
            {
                var qp = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);

                // TODO: Do some checks to make sure param is of expected type/format?
                if (qp.TryGetValue("initCount", out var cnt))
                {
                    // Navigate to `counter` page and pass in `initCount` param
                    nav.NavigateTo($"counter?initCount={cnt}");
                }
            }
        }
    }
 
    void IDisposable.Dispose()
    {
        appServices.AppLinkReceived -= AppServices_AppLinkReceived;
    }
}

Counter.razor

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

@functions {
    [Parameter]
    [SupplyParameterFromQuery(Name = "initCount")]
    public int currentCount { get; set; } = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

Note that the above code is completely untested, but I suspect it will generally work, maybe with a few tweaks. You can look at the docs to learn more about Blazor navigation. Also, while you could probably just parse out the url path and query string and have the built-in Blazor router handle it through the NavigateTo method (i.e. nav.NavigateTo(uri.PathAndQuery)), this would probably not be wise as it gives outside attackers a lot of power and flexibility with how they interact with your app (i.e. endpoints for deleting records), therefore it is better to parse the data coming from the outside and only interact with the minimal bits that you're expecting to be present.

Hope this helps!

@SamVanhoutte
Copy link

Thank you so much! Finally, I got this working! Right in time. Very much appreciated.

@prabhavmehra
Copy link

prabhavmehra commented Jul 11, 2023

@gerneio Did you face any issue when testing app link when the app is closed. I am unable to get that to work.

I have a similar setup with the exception:

  • I call OnNewIntent from OnCreate which then calls the App.Current.SendOnAppLinkRequestReceived(link); with the link being the Uri i get using Intent.DataString.

I get an exception

 android.runtime.JavaProxyThrowable: System.Reflection.TargetInvocationException: Arg_TargetInvocationException
                                                                                                     ---> System.InvalidOperationException: MauiContext should have been set on parent.

This is only when I try to open my app when its closed from the applink. When I have the app open and in background it works as expected and no crash.

Mostly because of some timing issue? Not too sure.

I did try something similar to yours, i.e. using a custom event handler, still the same error.
I am trying it on a MAUI app and not Blazor

Update: For anyone facing a similar issue, I solved the issue by moving to .net 8 preview. #9658 had a similar crash log and a fix assigned ( which is not back ported to .net 7), I thought I'll give it a try and that worked.

@Eilon
Copy link
Member

Eilon commented Dec 15, 2023

Hi everyone, our docs writers have published a doc on Deep Linking that you can read here: https://learn.microsoft.com/aspnet/core/blazor/hybrid/routing?view=aspnetcore-8.0&pivots=maui#deep-linking

@Eilon Eilon closed this as completed Dec 15, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Jan 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Blazor Hybrid / Desktop, BlazorWebView Cost:M Work that requires one engineer up to 2 weeks discussed Created by mkArtakMSFT to help with planning temporarily. It will be removed after planning is done. Priority:1 Created by mkArtakMSFT proposal/open t/docs 📝 User Story A single user-facing feature. Can be grouped under an epic.
Projects
None yet
Development

No branches or pull requests