0

I am trying to set up deep linking/universal linking for my .NET MAUI app. Everything appears on the surface to be set up according to the MS documentation, however when accessing links it simply uses the browser. This happens in both Android and Google.

The desired behaviour is so that any traffic to https://app.rackemapp.com (which will typically be through a QR code) will trigger the app to open.

In Android I tested it with a custom scheme of rackemapp:// which worked, so I know the base logic works, I just can't get the devices to understand that app.rackemapp.com traffic should open the app.

The curl testing to reach my AASA file works ok (no redirects or 40x errors) and the diagnostics tool on the device detects it as a valid universal link.

How do I make it open my app?

Update - Opening iOS required a new version number, that made it work. Android still a problem

MauiProgram.cs

public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .UseMauiCommunityToolkit()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("bootstrap-icons.ttf", "Bootstrap");
                })
                .ConfigureLifecycleEvents(lifecycle =>
                {
#if IOS
                    lifecycle.AddiOS(ios =>
                    {
                        // Universal link delivered to FinishedLaunching after app launch.
                        ios.FinishedLaunching((app, data) => HandleAppLink(app.UserActivity));

                        // Universal link delivered to ContinueUserActivity when the app is running or suspended.
                        ios.ContinueUserActivity((app, userActivity, handler) => HandleAppLink(userActivity));

                        // Only required if using Scenes for multi-window support.
                        if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13))
                        {
                            // Universal link delivered to SceneWillConnect after app launch
                            ios.SceneWillConnect((scene, sceneSession, sceneConnectionOptions)
                                => HandleAppLink(sceneConnectionOptions.UserActivities.ToArray()
                                    .FirstOrDefault(a => a.ActivityType == Foundation.NSUserActivityType.BrowsingWeb)));

                            // Universal link delivered to SceneContinueUserActivity when the app is running or suspended
                            ios.SceneContinueUserActivity((scene, userActivity) => HandleAppLink(userActivity));
                        }
                    });
#endif
#if ANDROID
                    lifecycle.AddAndroid(android =>
                    {
                        android.OnCreate((activity, bundle) =>
                        {
                            var action = activity.Intent?.Action;
                            var data = activity.Intent?.Data?.ToString();

                            if (action == Android.Content.Intent.ActionView && data is not null)
                            {
                                Task.Run(() => HandleAppLink(data));
                            }
                        });
                    });
#endif

                });

            //Unnecessary code omitted

            var app = builder.Build();

            ServiceHelper.Initialize(app.Services);

            return app;
        }

#if IOS
        static bool HandleAppLink(Foundation.NSUserActivity? userActivity)
        {
            if (userActivity is not null && userActivity.ActivityType == Foundation.NSUserActivityType.BrowsingWeb && userActivity.WebPageUrl is not null)
            {
                HandleAppLink(userActivity.WebPageUrl.ToString());
                return true;
            }
            return false;
        }
#endif

        static void HandleAppLink(string url)
        {
            if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
                App.Current?.SendOnAppLinkRequestReceived(uri);
        }

App.xaml.cs

public App()
        {

            InitializeComponent();           

            MainPage = new MainPage();
        }

        protected override async void OnAppLinkRequestReceived(Uri uri)
        {
            base.OnAppLinkRequestReceived(uri);

            var theurl = uri.ToString();

            // Show an alert to test that the app link was received.
            await Dispatcher.DispatchAsync(async () =>
            {
                //await Windows[0].Page!.DisplayAlert("App link received", uri.ToString(), "OK");
                var encodedString = theurl.Split("/")[4];

                byte[] data = Convert.FromBase64String(encodedString);
                string decodedString = System.Text.Encoding.UTF8.GetString(data);

                var api = "https://mobileapi.rackemapp.com";
#if DEBUG
                api = "https://g7c7crrc-44357.uks1.devtunnels.ms";
#endif
                var client = new RestClient(api);
                var request = new RestRequest("/mobile/getmatchfromtable/" + decodedString, Method.Get);

                var execute = client.Execute(request);

                string rawResponse = execute.Content;
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true
                };

                var response = new TableMatchResponse();

                if (rawResponse == null || rawResponse == "")
                {
                    response.MatchCode = "OFFLINE";
                }
                else
                {
                    response = System.Text.Json.JsonSerializer.Deserialize<TableMatchResponse>(rawResponse, options);
                }

                if (response.MatchCode == null || response.MatchCode == "")
                {
                    // error condition
                }
                else
                {
                    App.Current.MainPage.Navigation.PushModalAsync(new Scoreboard(response.MatchCode));
                }

            });
        }

curl to AASA

curl -v https://app.rackemapp.com/.well-known/apple-app-site-association
* Host app.rackemapp.com:443 was resolved.
* IPv6: (none)
* IPv4: 104.21.50.101, 172.67.204.183
*   Trying 104.21.50.101:443...
* Connected to app.rackemapp.com (104.21.50.101) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=rackemapp.com
*  start date: Dec 27 10:10:42 2024 GMT
*  expire date: Mar 27 11:08:16 2025 GMT
*  subjectAltName: host "app.rackemapp.com" matched cert's "*.rackemapp.com"
*  issuer: C=US; O=Google Trust Services; CN=WE1
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://app.rackemapp.com/.well-known/apple-app-site-association
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: app.rackemapp.com]
* [HTTP/2] [1] [:path: /.well-known/apple-app-site-association]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /.well-known/apple-app-site-association HTTP/2
> Host: app.rackemapp.com
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 200 
< date: Sat, 28 Dec 2024 21:57:37 GMT
< content-type: application/json
< content-length: 315
< last-modified: Tue, 24 Dec 2024 22:39:41 GMT
< accept-ranges: bytes
< etag: "589c11b95456db1:0"
< x-powered-by: ASP.NET
< cf-cache-status: DYNAMIC
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=oe3dB4x1fALVNQ6f%2FDzDNWMBUj772yNTdc7RsseLGWDPbOWJ4hgLRn3%2FitA%2BY4vBR4KN5juDa4x9wxu9z0CGbK4jXP5m0n6f0VTyKamvPIGlvEJl6VobT6dMJOdHMuiCqkMtMA%3D%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< strict-transport-security: max-age=0; includeSubDomains; preload
< x-content-type-options: nosniff
< x-cloudflare-enabled: true
< server: cloudflare
< cf-ray: 8f94ce9b09d271f2-LHR
< alt-svc: h3=":443"; ma=86400
< server-timing: cfL4;desc="?proto=TCP&rtt=41458&min_rtt=36685&rtt_var=18592&sent=5&recv=9&lost=0&retrans=0&sent_bytes=2911&recv_bytes=585&delivery_rate=50756&cwnd=227&unsent_bytes=0&cid=15096d52c33a80ae&ts=100&x=0"
< 
{
    "activitycontinuation": {
        "apps": [ "[Teamid].com.rackemapp.mobileapp" ]
    },
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "[Teamid].com.rackemapp.mobileapp",
                "paths": [ "*", "/*" ]
            }
        ]
    }
* Connection #0 to host app.rackemapp.com left intact

MainActivity.cs

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]

[IntentFilter(new[] { Android.Content.Intent.ActionView },
                        DataScheme = "https",
                        DataHost = "app.rackemapp.com",
                        DataPathPrefix = "/",
                        AutoVerify = true,
                        Categories = new[] { Intent.ActionView, Intent.CategoryDefault, Intent.CategoryBrowsable })]

public class MainActivity : MauiAppCompatActivity
{
    public MainActivity()
    {

    }
}

assetlinks.json

[
   {
      "relation": [
         "delegate_permission/common.handle_all_urls"
      ],
      "target": {
         "namespace": "RackEmAppMobile",
         "package_name": "com.rackemapp.mobileapp",
         "sha256_cert_fingerprints": [
            "C5:F1:29:4A:9D:95:41:F3:5A:0B:B9:DD:76:7A:F3:14:ED:D5:C0:17:D5:C8:F0:DB:B8:EC:70:E6:56:20:33:EC"
         ]
      }
   }
]
1
  • Thanks to this link stackoverflow.com/questions/32751225/… in the comments, I found I needed to bump the version number in order for iOS to recognise it. It hasn;t worked with Android though so still working on that Commented Dec 29, 2024 at 21:09

1 Answer 1

1

The guides provided in Microsoft Learn were largely correct, aside from 2 minor but important things that took a while to find.

  1. For iOS, it required that a new version number be applied in the csproj file in order for it to detect that universal linking was enabled. I've no idea why. Found the answer deep in the comments here: iOS Universal Links are not opening in-app

  2. For Android, the problem was in domain verification. Firstly, the assetlinks.json file needs to be UTF8 encoded. Secondly, the SHA256 you use in the assetlinks.json file needs to come from the adb command adb shell pm get-app-links com.package.name, whereas the guidance on the Microsoft articles were to get it from the keystore. Answer was found here: How to resolve Android get-app-links Returns State 1024?

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.