Conversation
- DeepLinkValidationService now detects CMS/PKCS7-signed AASA blobs (application/pkcs7-mime or DER 0x30) and extracts the inner JSON via SignedCms before parsing, with fallback to raw JSON. - AasaValidationResult gains a Signed flag; SimToolsTab shows '(signed)' on success when the AASA was served as a signed CMS blob. - Dispose JsonDocument instances in the validator. - SimToolsTab and DeviceToolsTab display an inline warning when the deep-link URL is a non-http(s) custom scheme, explaining that AASA / assetlinks.json validation requires an https URL while keeping Launch enabled so users can still try the scheme. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds in-app deep link validation utilities for iOS (AASA) and Android (assetlinks.json), surfaced in the simulator and device tools UI, backed by a new Core validation service.
Changes:
- Introduced
IDeepLinkValidationServiceplus result record types for AASA and assetlinks validation. - Implemented
DeepLinkValidationServiceto fetch and parse AASA (including signed CMS/PKCS7) andassetlinks.json. - Updated
SimToolsTabandDeviceToolsTabUI to launch deep links and display validation results (including raw JSON).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/MauiSherpa/Components/SimToolsTab.razor | Adds a Deep Links section for launching URLs and validating AASA with detailed result rendering. |
| src/MauiSherpa/Components/DeviceToolsTab.razor | Adds assetlinks.json validation UI and result rendering for device tools. |
| src/MauiSherpa.MacOS/MacOSMauiProgram.cs | Registers the new deep link validation service in the macOS head DI container. |
| src/MauiSherpa.Core/Services/DeepLinkValidationService.cs | New service that downloads and validates AASA and assetlinks association files. |
| src/MauiSherpa.Core/Interfaces.cs | Adds IDeepLinkValidationService and related result record types. |
| .validation-result { margin-top: 0.375rem; padding: 0.5rem; border-radius: 0.375rem; font-size: 0.6875rem; border: 1px solid var(--border-color); } | ||
| .validation-result.success { background: rgba(34,197,94,0.08); border-color: rgba(34,197,94,0.3); } | ||
| .validation-result.warning { background: rgba(234,179,8,0.08); border-color: rgba(234,179,8,0.3); } | ||
| .validation-result.error { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); } | ||
| .validation-status { display: flex; align-items: center; gap: 0.375rem; font-weight: 600; } | ||
| .validation-status .fa-check-circle { color: #22c55e; } | ||
| .validation-status .fa-exclamation-triangle { color: #eab308; } | ||
| .validation-status .fa-times-circle { color: #ef4444; } | ||
| .validation-entries { margin-top: 0.375rem; display: flex; flex-direction: column; gap: 0.25rem; } | ||
| .validation-entry { padding: 0.25rem 0.375rem; background: var(--bg-tertiary); border-radius: 0.25rem; } | ||
| .validation-entry strong { color: var(--text-primary); } | ||
| .validation-fingerprint { color: var(--text-secondary); margin-left: 0.375rem; font-family: 'Consolas', 'Monaco', monospace; font-size: 0.625rem; } | ||
| .validation-raw { margin-top: 0.375rem; } | ||
| .validation-raw summary { cursor: pointer; color: var(--text-secondary); font-size: 0.625rem; } | ||
| .validation-raw pre { margin: 0.25rem 0 0; padding: 0.375rem; background: var(--bg-tertiary); border-radius: 0.25rem; font-size: 0.625rem; overflow-x: auto; max-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; } | ||
| .deep-link-warning { margin-top: 0.375rem; padding: 0.375rem 0.5rem; border-radius: 0.375rem; font-size: 0.6875rem; background: rgba(234,179,8,0.08); border: 1px solid rgba(234,179,8,0.3); color: var(--text-primary); display: flex; gap: 0.375rem; align-items: flex-start; } |
There was a problem hiding this comment.
These validation styles (.validation-result, .validation-status, .validation-raw, .deep-link-warning, etc.) are duplicated between DeviceToolsTab and SimToolsTab. Consider moving them into a shared stylesheet (e.g., a common component CSS block or wwwroot CSS) to avoid future drift and reduce maintenance overhead.
| private static string? ExtractDomain(string url) | ||
| { | ||
| if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && | ||
| (uri.Scheme == "https" || uri.Scheme == "http")) | ||
| return uri.Host; | ||
| return null; | ||
| } | ||
|
|
||
| private static bool IsNonWebScheme(string url) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(url)) return false; | ||
| if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false; | ||
| return uri.Scheme != "https" && uri.Scheme != "http"; | ||
| } | ||
|
|
||
| private static string FormatJson(string json) | ||
| { | ||
| try | ||
| { | ||
| using var doc = System.Text.Json.JsonDocument.Parse(json); | ||
| return System.Text.Json.JsonSerializer.Serialize(doc, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); | ||
| } | ||
| catch { return json; } | ||
| } |
There was a problem hiding this comment.
ExtractDomain, IsNonWebScheme, and FormatJson are duplicated in both DeviceToolsTab and SimToolsTab. Consider factoring these into a shared helper (e.g., a small static utility in the UI project) so fixes/behavior changes (scheme handling, formatting options, etc.) only need to be made once.
| public async Task<AasaValidationResult> ValidateAppleAppSiteAssociationAsync(string domain) | ||
| { | ||
| try | ||
| { | ||
| var url = $"https://{domain}/.well-known/apple-app-site-association"; | ||
| var response = await Http.GetAsync(url).ConfigureAwait(false); | ||
|
|
There was a problem hiding this comment.
domain is interpolated directly into the URL string. If a caller passes unexpected input (e.g., containing @ userinfo or /), the request can be sent to an unintended host (e.g., https://example.com@evil.com/... resolves to evil.com). Validate that domain is a hostname (e.g., via Uri.CheckHostName and rejecting characters like @, /, :), or build the URI via UriBuilder to avoid URL-injection edge cases.
| public async Task<AasaValidationResult> ValidateAppleAppSiteAssociationAsync(string domain) | |
| { | |
| try | |
| { | |
| var url = $"https://{domain}/.well-known/apple-app-site-association"; | |
| var response = await Http.GetAsync(url).ConfigureAwait(false); | |
| static bool IsValidHostName(string domain) | |
| { | |
| if (string.IsNullOrWhiteSpace(domain)) | |
| return false; | |
| if (domain.IndexOfAny(['@', '/', '\\', ':', '?', '#']) >= 0) | |
| return false; | |
| return Uri.CheckHostName(domain) != UriHostNameType.Unknown; | |
| } | |
| public async Task<AasaValidationResult> ValidateAppleAppSiteAssociationAsync(string domain) | |
| { | |
| try | |
| { | |
| if (!IsValidHostName(domain)) | |
| { | |
| return new AasaValidationResult( | |
| false, | |
| false, | |
| null, | |
| Array.Empty<AasaAppEntry>(), | |
| "Invalid domain name", | |
| false); | |
| } | |
| var requestUri = new UriBuilder("https", domain) | |
| { | |
| Path = ".well-known/apple-app-site-association" | |
| }.Uri; | |
| var response = await Http.GetAsync(requestUri).ConfigureAwait(false); |
| static readonly HttpClient Http = new() | ||
| { | ||
| Timeout = TimeSpan.FromSeconds(10) | ||
| }; | ||
|
|
||
| public async Task<AasaValidationResult> ValidateAppleAppSiteAssociationAsync(string domain) | ||
| { | ||
| try | ||
| { | ||
| var url = $"https://{domain}/.well-known/apple-app-site-association"; | ||
| var response = await Http.GetAsync(url).ConfigureAwait(false); |
There was a problem hiding this comment.
This new service adds non-trivial parsing/validation logic (AASA modern/legacy formats, signed-vs-raw content, assetlinks relation filtering) but there are no accompanying unit tests under tests/MauiSherpa.Core.Tests/Services. Adding tests for success, missing/invalid JSON, non-2xx responses, and signed AASA decoding would help prevent regressions. Consider also making the HTTP dependency injectable (instead of a static HttpClient) to enable deterministic tests.
| static readonly HttpClient Http = new() | |
| { | |
| Timeout = TimeSpan.FromSeconds(10) | |
| }; | |
| public async Task<AasaValidationResult> ValidateAppleAppSiteAssociationAsync(string domain) | |
| { | |
| try | |
| { | |
| var url = $"https://{domain}/.well-known/apple-app-site-association"; | |
| var response = await Http.GetAsync(url).ConfigureAwait(false); | |
| readonly HttpClient http; | |
| static HttpClient CreateDefaultHttpClient() | |
| { | |
| return new HttpClient | |
| { | |
| Timeout = TimeSpan.FromSeconds(10) | |
| }; | |
| } | |
| public DeepLinkValidationService() | |
| : this(CreateDefaultHttpClient()) | |
| { | |
| } | |
| public DeepLinkValidationService(HttpClient httpClient) | |
| { | |
| http = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); | |
| } | |
| public async Task<AasaValidationResult> ValidateAppleAppSiteAssociationAsync(string domain) | |
| { | |
| try | |
| { | |
| var url = $"https://{domain}/.well-known/apple-app-site-association"; | |
| var response = await http.GetAsync(url).ConfigureAwait(false); |
| @inject SimInspectorService Inspector | ||
| @inject IAlertService AlertService | ||
| @inject IDialogService DialogService | ||
| @inject IDeepLinkValidationService DeepLinkValidator |
There was a problem hiding this comment.
IDeepLinkValidationService is injected here, but the main MAUI app host (src/MauiSherpa/MauiProgram.cs) does not register it in the DI container. This will throw at runtime when this component is rendered on Mac Catalyst/Windows. Register IDeepLinkValidationService (and its implementation) in the shared host (and any other heads that use these components), or guard the UI behind a feature check so it isn’t constructed when the service is unavailable.
| @inject DeviceInspectorService Inspector | ||
| @inject IAlertService AlertService | ||
| @inject IDialogService DialogService | ||
| @inject IDeepLinkValidationService DeepLinkValidator |
There was a problem hiding this comment.
IDeepLinkValidationService is injected here, but it isn’t registered in the main app DI container (src/MauiSherpa/MauiProgram.cs). Rendering this component will fail on Mac Catalyst/Windows unless the service is added to the shared registrations (and any other relevant heads).
| @inject IDeepLinkValidationService DeepLinkValidator |
| private static string? ExtractDomain(string url) | ||
| { | ||
| if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && | ||
| (uri.Scheme == "https" || uri.Scheme == "http")) | ||
| return uri.Host; | ||
| return null; |
There was a problem hiding this comment.
The toast/error messaging says users must enter an https:// URL, but ExtractDomain currently accepts both http and https. Either restrict this to https only, or update the messaging/warning text so it matches the accepted input.
| private static string? ExtractDomain(string url) | ||
| { | ||
| if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && | ||
| (uri.Scheme == "https" || uri.Scheme == "http")) |
There was a problem hiding this comment.
The UI messaging indicates assetlinks.json validation requires an https:// URL, but ExtractDomain accepts both http and https. Consider either enforcing https here, or adjusting the user-facing message to match the actual accepted schemes.
| (uri.Scheme == "https" || uri.Scheme == "http")) | |
| uri.Scheme == Uri.UriSchemeHttps) |
This pull request adds deep link validation tools for both iOS (AASA) and Android (assetlinks.json) to the device and simulator tools tabs. It introduces a new
IDeepLinkValidationServicefor validating domain association files, updates the UI to allow users to validate these files directly from the app, and displays detailed validation results with user-friendly feedback.Key changes include:
Core Service Implementation:
IDeepLinkValidationServiceand corresponding result record types (AasaValidationResult,AssetLinksValidationResult, etc.) to define methods for validating Apple App Site Association (AASA) and Android assetlinks.json files. (src/MauiSherpa.Core/Interfaces.cs)DeepLinkValidationServicewith logic to fetch, parse, and validate AASA and assetlinks.json files, handling both modern and legacy formats, and providing structured results. (src/MauiSherpa.Core/Services/DeepLinkValidationService.cs)DeepLinkValidationServiceas a singleton in the DI container for MacOS. (src/MauiSherpa.MacOS/MacOSMauiProgram.cs)UI Integration – Device Tools Tab:
IDeepLinkValidationServiceand added a new button to validate assetlinks.json from the device tools tab, showing a styled result section with summary, entry details, and raw JSON. (src/MauiSherpa/Components/DeviceToolsTab.razor) [1] [2] [3] [4] [5]UI Integration – Simulator Tools Tab:
IDeepLinkValidationServiceand added a new deep links section to the simulator tools tab, allowing users to launch deep links and validate AASA files with detailed feedback and raw JSON display. (src/MauiSherpa/Components/SimToolsTab.razor) [1] [2] [3] [4] [5]Initial commit