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 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.
Czym są deep links, Universal Links i App Links?
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.
Porównanie: URL Schemes vs Universal Links vs App Links
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.
Cecha
Custom URL Scheme
iOS Universal Links
Android App Links
Wymaga HTTPS?
Nie
Tak (ważny certyfikat)
Tak (ważny certyfikat)
Weryfikacja własności domeny
Brak
apple-app-site-association
assetlinks.json + SHA-256
Ryzyko przejęcia linku
Wysokie
Brak
Brak (przy autoVerify)
Fallback do przeglądarki
Nie
Tak
Tak
Minimalna wersja OS
iOS 2, Android 1
iOS 9
Android 6
Konfiguracja po stronie serwera
Brak
Wymagana
Wymagana
Wsparcie w .NET MAUI 10
Pełne
Pełne (Associated Domains)
Pełne (intent-filter)
Rekomendowane w 2026?
Tylko jako fallback
Tak (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.
Konfiguracja iOS Universal Links krok po kroku
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.
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:
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ą.
Konfiguracja Android App Links krok po kroku
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.
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:
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:
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.
Praktyczny przewodnik migracji z Xamarin.Forms do .NET MAUI 10 w 2026: Upgrade Assistant, przepisanie Handlers, DI zamiast DependencyService, Shell, publikacja w App Store i Google Play.
Praktyczny przewodnik po architekturze offline-first w .NET MAUI. Konfiguracja SQLite z EF Core, wykrywanie sieci, trzy wzorce synchronizacji danych i zadania w tle z WorkManager i Shiny — z gotowymi przykładami kodu.
Praktyczny przewodnik po architekturze MVVM w .NET MAUI. CommunityToolkit.Mvvm 8.4 z partial properties, wstrzykiwanie zależności, nawigacja Shell i testy jednostkowe — wszystko, czego potrzebujesz do budowania profesjonalnych aplikacji mobilnych.