Deep linking w .NET MAUI 10: iOS Universal Links i Android App Links (przewodnik 2026)

Kompletny przewodnik po deep linkach w .NET MAUI 10: konfiguracja Universal Links na iOS, App Links na Androidzie, routing w Shell i najczęstsze błędy weryfikacji AASA oraz assetlinks.json. Z gotowym kodem, przykładami i checklistą do debugowania.

Deep Linking .NET MAUI 10 - Przewodnik 2026

Zaktualizowano: 11 września 2026

Deep linking w .NET MAUI 10 to mechanizm otwierania konkretnego ekranu aplikacji z zewnętrznego URL. Na iOS realizowany jest przez Universal Links (skojarzone domeny), a na Androidzie przez App Links (weryfikowane intencje HTTPS). W obu przypadkach potrzebujesz publicznie dostępnego pliku metadanych pod adresem /.well-known/, wpisu w konfiguracji projektu (Entitlements na iOS, intent-filter na Androidzie) oraz routingu w AppShell. W tym przewodniku pokażę dokładnie, co dzieje się pod maską, bo połowa poradników zatrzymuje się na Shell.Current.GoToAsync i zapomina o tym, że jedna literówka w apple-app-site-association potrafi zablokować deep linki na dwa tygodnie. Sam się o tym przekonałem, wypuszczając aplikację e-commerce w zeszłym roku.

  • iOS Universal Links wymagają pliku apple-app-site-association podpisanego przez serwer HTTPS z ważnym certyfikatem oraz Associated Domains w Entitlements (applinks:twojadomena.pl).
  • Android App Links opierają się na pliku assetlinks.json z odciskiem SHA-256 klucza podpisującego APK/AAB oraz atrybucie android:autoVerify="true" w AndroidManifest.
  • Bez HTTPS, ważnego certyfikatu i poprawnego Content-Type (application/json) system operacyjny cicho zignoruje weryfikację. Aplikacja otworzy się tylko przez wybór w chooserze zamiast automatycznie.
  • Routing w .NET MAUI realizujemy przez Routing.RegisterRoute() i Shell.Current.GoToAsync(), natomiast zdarzenie wejściowe łapiemy w App.OnAppLinkRequestReceived lub przez platformowe OpenUrl.
  • Testowanie: xcrun simctl openurl na symulatorze iOS, adb shell am start -a android.intent.action.VIEW -d URL na Androidzie, plus narzędzie App Links Assistant w Android Studio.
  • Custom URL schemes (myapp://) nadal działają jako fallback, ale nie zapewniają weryfikacji ani nie chronią przed przechwyceniem linku przez inną aplikację. Traktuj je jako plan B, nie A.

Deep link to URL, który zamiast otwierać stronę w przeglądarce, prowadzi bezpośrednio do konkretnego widoku w aplikacji mobilnej. W ekosystemie natywnym istnieją trzy warianty tej idei: custom URL schemes (schemat myapp://profil/42), iOS Universal Links oraz Android App Links. Pierwsza wersja to relikt z ery iOS 8 i Androida 5. Działa, ale nie ma żadnej weryfikacji, więc dowolna inna aplikacja może zarejestrować ten sam schemat i przejąć ruch.

Universal Links (Apple, iOS 9+) oraz App Links (Google, Android 6+) rozwiązują problem przejęcia w ten sposób, że system operacyjny sam weryfikuje własność domeny za pomocą pliku podpisu na Twoim serwerze HTTPS. Gdy użytkownik kliknie link typu https://sklep.przyklad.pl/produkty/123, iOS i Android sprawdzają, czy w chwili instalacji Twojej aplikacji pobrany plik potwierdza, że to Ty jesteś prawnym właścicielem domeny. Dopiero wtedy link otwiera się w aplikacji, a nie w Safari czy Chrome.

W praktyce oznacza to, że deep linking to nie tylko funkcja UX. To także element bezpieczeństwa. Jeśli Twoja aplikacja obsługuje płatności, resetowanie hasła albo linki magic-login z e-maila, custom scheme po prostu nie wystarczy. Apple w dokumentacji Supporting associated domains otwarcie odradza używanie custom schemes do wrażliwych flow.

Zanim wybierzesz wariant, warto zobaczyć na jednym rzucie oka, co dostajesz w każdej z opcji. Poniższa tabela pochodzi z moich doświadczeń z aplikacjami produkcyjnymi, w których wspieramy oba systemy jednocześnie.

CechaCustom URL SchemeiOS Universal LinksAndroid App Links
Wymaga HTTPS?NieTak (ważny certyfikat)Tak (ważny certyfikat)
Weryfikacja własności domenyBrakapple-app-site-associationassetlinks.json + SHA-256
Ryzyko przejęcia linkuWysokieBrakBrak (przy autoVerify)
Fallback do przeglądarkiNieTakTak
Minimalna wersja OSiOS 2, Android 1iOS 9Android 6
Konfiguracja po stronie serweraBrakWymaganaWymagana
Wsparcie w .NET MAUI 10PełnePełne (Associated Domains)Pełne (intent-filter)
Rekomendowane w 2026?Tylko jako fallbackTak (produkcja)Tak (produkcja)

Wniosek jest prosty: w każdej nowej aplikacji domyślnym wyborem powinny być Universal Links i App Links z tego samego adresu HTTPS, a custom scheme trzymamy tylko dla ekstremalnych edge case'ów (np. połączenia międzyaplikacyjne w kontrolowanym ekosystemie firmowym). Jeśli planujesz też komunikację server-driven, warto to skoordynować z Twoją architekturą MVVM w .NET MAUI i nawigacją Shell, bo docelowo deep link musi trafić do dokładnie tego samego ViewModel, do którego trafiłby normalny push nawigacyjny.

Zacznij od pliku apple-app-site-association. To zwykły JSON serwowany pod adresem https://twojadomena.pl/.well-known/apple-app-site-association. Kluczowe rzeczy: brak rozszerzenia .json, Content-Type: application/json i odpowiedź bez przekierowań (301/302). Apple pobiera ten plik przy instalacji aplikacji oraz (od iOS 14) przez usługę CDN Apple, więc lokalne pliki hosts nie pomogą w testach na urządzeniu.

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.pl.przyklad.sklep"],
        "components": [
          {
            "/": "/produkty/*",
            "comment": "Otwiera szczegóły produktu"
          },
          {
            "/": "/koszyk",
            "comment": "Otwiera koszyk"
          }
        ]
      }
    ]
  }
}

Wartość appIDs to TeamID.BundleID. TeamID znajdziesz w portalu Apple Developer Account pod nagłówkiem Membership. Bundle ID to identyfikator z pliku Info.plist lub z <ApplicationId> w csproj.

Następnie edytuj Platforms/iOS/Entitlements.plist i dodaj:

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:twojadomena.pl</string>
</array>

W pliku csproj upewnij się, że w konfiguracji Release dla iOS masz <CodesignEntitlements>Platforms\iOS\Entitlements.plist</CodesignEntitlements>. To krok, o którym zapomina połowa deweloperów migrujących z Xamarin.Forms. Tam entitlements były w innym miejscu i nie zawsze przenoszą się automatycznie. Jeśli akurat robisz taką migrację, pomocny będzie osobny przewodnik po migracji Xamarin.Forms do .NET MAUI 10, gdzie omawiam pułapki związane z konfiguracją projektową.

Android używa pliku assetlinks.json pod adresem https://twojadomena.pl/.well-known/assetlinks.json. Format opisuje standard Digital Asset Links dla Android App Links. Kluczowa jest sekcja sha256_cert_fingerprints, czyli odcisk klucza podpisującego wersję Release. Nie może to być klucz debug ani upload key z Play App Signing.

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "pl.przyklad.sklep",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
    ]
  }
}]

Jak zdobyć SHA-256? Jeśli używasz Play App Signing (a od 2021 Google to praktycznie wymóg dla nowych aplikacji), znajdziesz odcisk w Google Play Console → App Integrity → App signing key certificate. Warto tam sprawdzić oba odciski: upload i signing. Dla developmentu potrzebujesz upload key, ale dla produkcji zawsze signing key. Możesz spokojnie dodać oba do tablicy sha256_cert_fingerprints.

Teraz w Platforms/Android/AndroidManifest.xml dodaj do głównej Activity intent-filter:

<activity ...>
  <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="twojadomena.pl" />
  </intent-filter>
</activity>

Atrybut android:autoVerify="true" to sedno mechanizmu. Mówi systemowi, że przy instalacji aplikacji ma automatycznie pobrać assetlinks.json i zweryfikować podpis. Bez tego atrybutu Android otworzy chooser ("Otwórz w:") zamiast bezpośrednio Twojej aplikacji. Od Androida 12 (API 31) domyślnie tylko zweryfikowane linki otwierają aplikację automatycznie, więc autoVerify nie jest już opcjonalne.

Obsługa linków w kodzie .NET MAUI (Shell + routing)

Konfiguracja platformowa to połowa roboty. Druga połowa to routing wewnątrz aplikacji. W .NET MAUI 10 rekomendowanym podejściem jest Shell z rejestracją tras. Zacznijmy od zarejestrowania tras w konstruktorze AppShell:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute("produkty/szczegoly", typeof(ProductDetailPage));
        Routing.RegisterRoute("koszyk", typeof(CartPage));
    }
}

Teraz w App.xaml.cs nadpisz metodę OnAppLinkRequestReceived, która jest wywoływana zarówno przy Universal Link (iOS), jak i App Link (Android):

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

    if (uri.Host != "twojadomena.pl")
        return;

    var segments = uri.AbsolutePath.Trim('/').Split('/');

    if (segments.Length == 2 && segments[0] == "produkty")
    {
        var productId = segments[1];
        await Shell.Current.GoToAsync(
            $"//produkty/szczegoly?id={Uri.EscapeDataString(productId)}");
    }
    else if (segments.Length == 1 && segments[0] == "koszyk")
    {
        await Shell.Current.GoToAsync("//koszyk");
    }
}

Docelowa ProductDetailPage odbiera parametr przez atrybut QueryPropertyAttribute na kodzie code-behind albo (czystsze rozwiązanie) na ViewModelu wstrzykniętym przez DI. Podwójny slash // w ścieżce resetuje stos nawigacji do korzenia, co jest zwykle pożądane przy wejściu z linku zewnętrznego. Jeśli chcesz zachować bieżący kontekst (np. użytkownik był już w aplikacji), użyj pojedynczego /.

W .NET MAUI 10 dodano też natywne wsparcie dla WebAuthenticator callbacks przez Universal Links, co eliminuje osobny custom scheme dla OAuth. Jeśli robisz uwierzytelnianie, przy okazji zerknij, jak łączyć to z bezpieczeństwem aplikacji .NET MAUI i SecureStorage. Token przechowuj w Keychain/Keystore, a nie w preferences.

Jak testować deep linki na iOS i Androidzie?

Testowanie deep linków to obszar, w którym najwięcej deweloperów się potyka, bo klikanie w link z Notatek albo z Safari zachowuje się inaczej niż otwarcie przez terminal. Honestly, sam wpadam w tę pułapkę co kilka miesięcy. Poniżej zestaw komend, których używam codziennie.

iOS, symulator

# Otwórz Universal Link w symulatorze
xcrun simctl openurl booted https://twojadomena.pl/produkty/123

# Sprawdź, czy plik AASA jest poprawnie pobrany
xcrun simctl spawn booted swcutil dl

iOS, urządzenie fizyczne

Na urządzeniu użyj Apple's App Search API Validation Tool. Wpisujesz URL i widzisz, jak Apple pobiera i parsuje Twój plik. Jeśli plik ma błąd składni JSON albo zły Content-Type, tool od razu to pokaże. Alternatywnie, po zainstalowaniu aplikacji przez Xcode włącz w Settings → Developer → Universal Links → Diagnostics, żeby zobaczyć status per domena.

Android, emulator i urządzenie

# Otwórz App Link
adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d "https://twojadomena.pl/produkty/123"

# Sprawdź status weryfikacji dla Twojej aplikacji
adb shell pm get-app-links pl.przyklad.sklep

# Wymuś ponowną weryfikację (po zmianie assetlinks.json)
adb shell pm verify-app-links --re-verify pl.przyklad.sklep

Dla pełnej pewności, że testy end-to-end nie zepsują się przy kolejnym wydaniu, warto pokryć deep linki testami UI. Jak to zorganizować w praktyce, opisałem w przewodniku o testowaniu aplikacji .NET MAUI z Appium i xUnit. Appium potrafi wystartować aplikację z konkretnym URI capability, co świetnie nadaje się do smoke testów deep linków w CI.

Najczęstsze błędy i jak je diagnozować

Po siedmiu latach shipowania aplikacji z deep linkami mam listę pułapek, w które wpadają praktycznie wszyscy. Oto pięć najczęstszych. Trafiłem na każdą z nich osobiście, więc mówię z doświadczenia, nie z lektury dokumentacji.

1. Serwer zwraca 301 przy pobieraniu AASA

Apple nie podąża za przekierowaniami przy pobieraniu apple-app-site-association. Jeśli Twój CDN robi redirect z http:// na https:// albo z www na apex, plik nie zostanie pobrany. Rozwiązanie: dodaj domenę bez www do Associated Domains i skonfiguruj serwer tak, żeby /.well-known/apple-app-site-association był serwowany bezpośrednio ze statusem 200.

2. Zły Content-Type dla AASA

Plik musi być serwowany jako application/json. Nginx często domyślnie serwuje pliki bez rozszerzenia jako application/octet-stream. Fix w Nginx:

location = /.well-known/apple-app-site-association {
    default_type application/json;
}

3. SHA-256 z debug key zamiast release key na Androidzie

Klasyk. Deweloper testuje z Android Studio (debug key), wszystko działa, po opublikowaniu w Google Play przestaje. Google Play używa własnego signing key (Play App Signing) i to jego odcisk musi być w assetlinks.json. Dodaj oba: upload key i signing key.

4. Brak weryfikacji na Androidzie 12+

Od Androida 12 użytkownik musi ręcznie potwierdzić otwieranie linków w aplikacji, jeśli weryfikacja się nie powiodła. Bez autoVerify="true" i poprawnego assetlinks.json nawet po instalacji z Play link otworzy się w Chrome. Zawsze sprawdzaj pm get-app-links po pierwszej instalacji.

5. Deep link zabija stos nawigacji

Ten problem dotyczy głównie .NET MAUI. Jeśli w OnAppLinkRequestReceived używasz Shell.Current.GoToAsync("//sciezka"), resetujesz stos nawigacji. Jeśli aplikacja była w tle w środku innego flow (np. checkout), użytkownik straci kontekst. Rozważ trzymanie pending navigation w statycznym property i wykonanie go po zakończeniu obecnej akcji użytkownika.

Najczęściej zadawane pytania

Czy Universal Links wymagają certyfikatu SSL?

Tak, Apple wymaga ważnego certyfikatu HTTPS dla domeny serwującej apple-app-site-association. Certyfikat self-signed nie zadziała nawet w developmencie. Let's Encrypt jest w zupełności wystarczający dla produkcji.

Jaka jest różnica między deep linkiem a Universal Link?

Deep link to ogólne pojęcie dla URL prowadzącego do konkretnego widoku w aplikacji. Universal Link to konkretna implementacja Apple używająca zwykłego HTTPS URL i weryfikacji własności domeny. Custom scheme (myapp://) to też deep link, ale bez weryfikacji.

Czy jeden URL może obsłużyć iOS i Android?

Tak, i to jest największa zaleta Universal Links plus App Links. Ten sam URL https://twojadomena.pl/produkty/123 otworzy aplikację na iOS, aplikację na Androidzie, a jeśli aplikacja nie jest zainstalowana, stronę WWW. Potrzebujesz obu plików (apple-app-site-association i assetlinks.json) pod /.well-known/.

Dlaczego mój App Link nie otwiera aplikacji tylko przeglądarkę?

Prawdopodobnie weryfikacja się nie powiodła. Uruchom adb shell pm get-app-links <package>. Jeśli status to none lub error, sprawdź: (1) czy assetlinks.json jest dostępny publicznie po HTTPS, (2) czy SHA-256 zgadza się z kluczem podpisującym APK, (3) czy w manifeście masz android:autoVerify="true".

Czy custom URL schemes są nadal wspierane w .NET MAUI 10?

Tak, w pełni. Custom scheme jest przydatny w izolowanych ekosystemach (kioski, aplikacje wewnętrzne firm) oraz jako fallback dla starszych systemów. W .NET MAUI 10 konfigurujesz go przez CFBundleURLTypes w Info.plist (iOS) i <data android:scheme="myapp" /> w intent-filter (Android). Nie używaj do flow wrażliwych na bezpieczeństwo.

Jak przetestować deep link zanim aplikacja trafi do sklepu?

Na iOS: użyj TestFlight lub Ad Hoc distribution, Universal Links działają identycznie. Na Androidzie: zbuduj wersję Release podpisaną tym samym kluczem co produkcja, zainstaluj przez ADB, potem adb shell am start -a android.intent.action.VIEW -d URL. Nie testuj Universal Links w Safari, Apple blokuje otwieranie aplikacji z tej samej domeny po kliknięciu w Safari.

David O'Reilly
O Autorze David O'Reilly

Native iOS/Android specialist turned MAUI advocate. Writes about the gritty platform details most cross-platform tutorials skip.