Onboarding guide
26. Device Capability Abstraction Layer (Native Contracts, MAUI, Browser & Fallback Adapters)
What this chapter covers. One Blazor component library in MMCA.Common.UI renders on three very
different heads: Blazor Server (server-side prerender plus interactive Server circuits), Blazor
WebAssembly (the whole component tree running inside the browser), and MAUI Blazor Hybrid (the same
components inside a native shell on Android, iOS, Windows and macOS). Those heads have wildly
different access to the device. A phone can vibrate, scan a fingerprint, read a QR code with the
camera, drop a local notification and open the system share sheet; a server-rendered page can do none
of that; a browser page can do some of it through web APIs. This group is how one component library
talks to all of that hardware without ever naming a platform type. It is a set of small,
single-capability interface contracts (biometrics, geolocation and geocoding, speech, push
registration, media pick, barcode scanning, clipboard, screenshot, haptics, share, external links,
external OAuth, local cache, local notifications, connectivity, battery, accessibility announcements,
deep links) plus three families of adapters that implement each contract per host: MAUI-native,
browser-JS-interop, and inert fallback. The head chooses which family it resolves at DI composition
time. This is the [Rubric §18, UI Architecture] and [Rubric §22, Responsive/Cross-Browser] story
in miniature, and the design is
ADR-042
(Website/docs-src/adr/042-device-capability-abstraction.md).
The contract-per-capability shape. Every capability is its own narrow interface in the
MMCA.Common.UI.Services.Capabilities namespace (form-factor detection, IFormFactor,
sits one level up in MMCA.Common.UI.Services). The contracts are deliberately tiny and
transport-agnostic: they speak in booleans, strings, and framework-owned types, never in a MAUI or a
JS type. IBiometricAuthenticator
(MMCA.Common.UI/Services/Capabilities/Auth/IBiometricAuthenticator.cs:9) is the clearest example of the
house rule. Availability and outcome are both plain Task<bool>
(IBiometricAuthenticator.cs:12,19), and every failure mode (cancellation, lockout, error) collapses
to false, documented right on the member (IBiometricAuthenticator.cs:14-18), so a caller can only
fall back to the normal credential login, never to a weaker path. Where a capability must return
structured data it does so through a framework-owned type rather than a platform one:
GeoPoint (MMCA.Common.UI/Services/Capabilities/Geo/GeoPoint.cs:9) is a sealed record
latitude/longitude pair that even carries its own haversine DistanceKmTo helper
(GeoPoint.cs:17-29, over an EarthRadiusKm of 6371.0 at GeoPoint.cs:11) so shared components
never touch a platform location type. PickedMedia
(MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:29, deliberately a class rather than a
record because a record's generic IEquatable<T> is a generic WinRT interface that trips CsWinRT AOT
generation (CsWinRT1030) on the windows TFM, IMediaPickerService.cs:22-24),
PushDeviceToken
(MMCA.Common.UI/Services/Capabilities/Notifications/IPushDeviceTokenProvider.cs:19), and
LocalNotificationRequest
(MMCA.Common.UI/Services/Capabilities/Notifications/LocalNotificationRequest.cs:17) play the same role for their
capabilities. Keeping the contracts in the shared UI layer and the platform types out of them is the
[Rubric §1, SOLID] dependency-inversion move that makes the whole layer swappable per host.
Composition: safe defaults first, head overrides last. The wiring is a two-phase, last-wins
registration and it is the load-bearing mechanism of the group. AddUIShared (in the wider UI group)
calls AddDeviceCapabilityDefaults
(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:37), which TryAdd-registers a
neutral implementation for every contract, so any shared component can resolve any capability on
any head and get a well-defined no-op rather than a missing-service exception
(DependencyInjection.cs:40-78). That method is public for a second reason spelled out on it: a
consumer's bUnit test base registers the same set the production host gets instead of mirroring the
list by hand, because a hand-mirrored list rots the moment a new contract ships and the component
test fails with a DI resolution error rather than a useful one (DependencyInjection.cs:30-35,
[Rubric §14, Testability]). A head then calls its own registration after AddUIShared with
plain Add calls, and because the last single-service registration wins, those override the
defaults, a rule spelled out on both DI classes (DependencyInjection.cs:15-22,
MMCA.Common.UI.Maui/DependencyInjection.cs:25-31). Browser heads call
AddBrowserDeviceCapabilities (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:91);
native heads call AddMauiDeviceCapabilities (MMCA.Common.UI.Maui/DependencyInjection.cs:42), which
ships in the separate MAUI-TFM package MMCA.Common.UI.Maui, the one package built outside
MMCA.Common.slnx
(ADR-042). Both DI
classes use the C# extension(IServiceCollection) member idiom this codebase favors for
registration (DependencyInjection.cs:34, MMCA.Common.UI.Maui/DependencyInjection.cs:34; see the
primer). The lifetime choices are deliberate and
commented in place: the browser services are Scoped, one per Blazor circuit, so per-user state
never leaks across circuits (DependencyInjection.cs:102-112), while the MAUI services are Singleton
because a native head is single-user and its stateful services (connectivity, battery) wrap
app-global platform events (MMCA.Common.UI.Maui/DependencyInjection.cs:44-62).
Three adapter families. Each contract has up to three implementations, split across three
namespaces. The fallback family lives in MMCA.Common.UI.Services.Capabilities.Fallbacks and is
the Null Object pattern applied wholesale ([Rubric §2, Design Patterns]):
NullBiometricAuthenticator
(MMCA.Common.UI/Services/Capabilities/Auth/NullBiometricAuthenticator.cs:4) simply returns
false from both members (NullBiometricAuthenticator.cs:7-12),
NullShareService, NullClipboardService and their
siblings do nothing, and
AlwaysOnlineConnectivityStatusService reports permanent
connectivity through an event whose add/remove accessors are deliberately empty because it is never
raised
(MMCA.Common.UI/Services/Capabilities/DeviceStatus/AlwaysOnlineConnectivityStatusService.cs:10-27),
which is the correct answer on Blazor Server, where a lost connection tears down the circuit itself
(MMCA.Common.UI/Services/Capabilities/DeviceStatus/IConnectivityStatusService.cs:3-9). These are what make it
safe for a shared component to call a capability unconditionally: the null implementation answers
"not available here" honestly and the component hides the corresponding affordance. The MAUI
family lives in MMCA.Common.UI.Maui.Capabilities and wraps the real platform APIs:
MauiFormFactor (MMCA.Common.UI.Maui/Capabilities/MauiFormFactor.cs:12) reads
DeviceInfo.Idiom and DeviceInfo.Platform (MauiFormFactor.cs:15,18), and its siblings drive MAUI
Essentials, the MAUI Community Toolkit, Plugin.LocalNotification, and ZXing.Net.MAUI. Assistive
technology is the tidiest illustration of all three families at once:
IAccessibilityAnnouncer
(MMCA.Common.UI/Services/Capabilities/Accessibility/IAccessibilityAnnouncer.cs:9) is a one-method contract for
speaking events a sighted user perceives visually, a live poll opening or the unread badge
incrementing (IAccessibilityAnnouncer.cs:3-8);
BrowserAccessibilityAnnouncer writes into a visually hidden
aria-live="polite" region created on first use by the shared JS module
(MMCA.Common.UI/Services/Capabilities/Accessibility/BrowserAccessibilityAnnouncer.cs:4-6,16-19),
MauiAccessibilityAnnouncer forwards to SemanticScreenReader.Default
and swallows FeatureNotSupportedException where no screen-reader integration exists
(MMCA.Common.UI.Maui/Capabilities/Accessibility/MauiAccessibilityAnnouncer.cs:16-21), and
NullAccessibilityAnnouncer is the silent default
(MMCA.Common.UI/Services/Capabilities/Accessibility/NullAccessibilityAnnouncer.cs:4,7). One call site,
right behavior on all three heads, which is [Rubric §21, Accessibility] bought at the composition
layer instead of page by page.
The browser family and its prerender-safe contract. The browser family lives in
MMCA.Common.UI.Services.Capabilities.Browser and reaches the device through JavaScript interop, but
it never calls IJSRuntime directly. Every browser service depends on
CapabilitiesJsModule
(MMCA.Common.UI/Services/Capabilities/CapabilitiesJsModule.cs:12), a lazy accessor built
over LazyJsModule for the single
./_content/MMCA.Common.UI/capabilities-interop.js module (CapabilitiesJsModule.cs:14,19)
registered once per circuit (DependencyInjection.cs:103). Its InvokeOrDefaultAsync<T>
(CapabilitiesJsModule.cs:26) is the degradation contract that makes browser capabilities usable
during server-side prerender: it wraps the import-and-invoke in a try that swallows the entire
JS-unavailable exception family (InvalidOperationException for an un-hydrated prerender,
JSDisconnectedException for a torn-down circuit, and JSException for a throwing browser API) and
returns default (CapabilitiesJsModule.cs:36-48); disposal delegates to the same lazy module
(CapabilitiesJsModule.cs:52). So BrowserShareService
(MMCA.Common.UI/Services/Capabilities/Interop/BrowserShareService.cs:8) invoking shareLink
against navigator.share reads the nullable result as shared == true
(BrowserShareService.cs:20-23) and degrades to "did not share" during prerender instead of
throwing, exactly as the null implementation would; file sharing has no browser primitive at all, so
it answers false outright (BrowserShareService.cs:27-28). That is the
[Rubric §22, Responsive/Cross-Browser] and [Rubric §23, Front-End Performance] discipline which
lets the same component prerender on the server and hydrate in the browser with no capability check
at every call site.
Form-factor detection across the trio. IFormFactor
(MMCA.Common.UI/Services/IFormFactor.cs:7) is the smallest capability, two strings describing the
device and the platform (IFormFactor.cs:10,13), and it is the one contract with three genuinely
different, hoisted implementations: WebFormFactor
(MMCA.Common.UI.Web/Services/WebFormFactor.cs:12) reports "Web" for the server head
(WebFormFactor.cs:15), WasmFormFactor
(MMCA.Common.UI/Services/WasmFormFactor.cs:9) reports "WebAssembly" for the browser runtime
(WasmFormFactor.cs:12), and MauiFormFactor reports the real device idiom plus
platform and version (MauiFormFactor.cs:15,18). Each head registers its own, and
AddMauiFormFactor (MMCA.Common.UI.Maui/DependencyInjection.cs:143-144) is kept deliberately
separate from the capability bundle so a head that still registers its own implementation keeps
last-registration-wins control (MMCA.Common.UI.Maui/DependencyInjection.cs:137-142). The trio is
the concrete illustration of why this whole group exists: identical shared components read
GetFormFactor() and GetPlatform() and adapt, and the answer to "what am I running on" is
injected, not detected inline.
Camera scanning, the opt-in capability. Barcode and QR scanning is the one capability that even a
native head does not get by default, and it shows the composition rule taken one step further.
IBarcodeScannerService
(MMCA.Common.UI/Services/Capabilities/Media/IBarcodeScannerService.cs:11) is two members, an IsSupported
flag and a ScanAsync that returns the decoded payload or null
(IBarcodeScannerService.cs:14,20); a denied permission, a cancelled scan, an unsupported head, and a
cancelled token all collapse to the same null, and the contract states outright that the scanned
payload is untrusted input to validate before acting on it
(IBarcodeScannerService.cs:3-10, [Rubric §26, Front-End Security]). Browsers have no shared
camera-scanning primitive, so web heads keep NullBarcodeScannerService
(MMCA.Common.UI/Services/Capabilities/Media/NullBarcodeScannerService.cs:9) and hide the button
(NullBarcodeScannerService.cs:12). On MAUI the override is opt-in through UseCommonBarcodeScanner
rather than folded into the bundle, because a head that never scans should ship neither the ZXing
camera handler nor a camera permission declaration
(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:86-87,104-115, and the default's own comment at
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:67-70).
MauiBarcodeScannerService
(MMCA.Common.UI.Maui/Capabilities/Media/MauiBarcodeScannerService.cs:24) reports support on Android and
iOS only (MauiBarcodeScannerService.cs:51-53), then pushes BarcodeScanPage
modally over the current window page and pops it on every exit path
(MauiBarcodeScannerService.cs:94-102). The page itself is built in code rather than XAML so the
package ships no compiled resource dictionary, restricts decoding to two-dimensional formats to cut
false positives on a shaky handheld frame, and resolves a single TaskCompletionSource exactly once
from first decode, cancel button, back gesture, or disappearance
(MMCA.Common.UI.Maui/Capabilities/Media/BarcodeScanPage.cs:21,23-24,36,72-89). The subtlest detail is
localization: the two page strings are passed as Func<string> delegates and invoked once per scan,
because the singleton service is constructed while the app is being built, which is before
MauiCultureInitializer restores the user's persisted language, so
resolving the text at construction would pin the modal to the device language forever
(MauiBarcodeScannerService.cs:16-22,67-68; the registration takes only the delegate form, so there
is no way to hand it fixed text, MMCA.Common.UI.Maui/HostingDependencyInjection.cs:104-113). That
is [Rubric §27, i18n] showing up inside a device capability.
Deep links: one funnel from native navigation into Blazor routing. The most involved runtime flow
in this group is the deep-link path
(ADR-043,
Website/docs-src/adr/043-mobile-deep-links-and-native-oauth-callback.md).
IDeepLinkDispatcher
(MMCA.Common.UI/Services/Capabilities/Navigation/IDeepLinkDispatcher.cs:10) is the single boundary between
native navigation sources (notification taps, home-screen app actions, app links, QR scans) and the
Blazor router. Native code calls Publish(route) with an app-relative route
(IDeepLinkDispatcher.cs:19); the shared DeepLinkListener component (in the UI-components group)
either receives it live via the RouteRequested event (IDeepLinkDispatcher.cs:13) or drains it
from a buffer after first render (IDeepLinkDispatcher.cs:22). The default
DeepLinkDispatcher
(MMCA.Common.UI/Services/Capabilities/Navigation/DeepLinkDispatcher.cs:9) is registered as a singleton
(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:78) because native callers publish
into it from outside any scope, and it solves the cold-start race: Publish reads the handler and
writes the single-entry, last-write-wins buffer inside one Lock, because doing the two as separate
steps allowed an interleaving that dropped the route entirely on a warm-boot deep link, where the
native callback thread and the first render are genuinely concurrent
(DeepLinkDispatcher.cs:11,22-45); the listener drains it via TryConsumePending under the same
lock once it renders (DeepLinkDispatcher.cs:48-57), and the handler is invoked outside the lock so
a listener that navigates on the callback never runs under it (DeepLinkDispatcher.cs:43-44). The
event payload is DeepLinkRouteEventArgs, a one-property EventArgs
carrying the app-relative route (MMCA.Common.UI/Services/Capabilities/Navigation/DeepLinkRouteEventArgs.cs:4-10).
On MAUI the bridge is wired by DeviceCapabilitiesInitializer
(MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:14), an IMauiInitializeService that hooks
LocalNotificationCenter.Current.NotificationActionTapped
(DeviceCapabilitiesInitializer.cs:27) and republishes the tapped notification's ReturningData
route into the dispatcher, skipping dismissals (DeviceCapabilitiesInitializer.cs:30-42). The other
end of that loop is MauiLocalNotificationService, which copies a
LocalNotificationRequest's DeepLinkRoute into the platform request's
ReturningData when it schedules
(MMCA.Common.UI.Maui/Capabilities/Notifications/MauiLocalNotificationService.cs:52). All of that wiring is
installed by UseMauiDeviceCapabilities on the MauiAppBuilder
(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:30), which also calls UseLocalNotification()
and registers the native capability bundle (HostingDependencyInjection.cs:32-34).
Culture is applied through the same last-wins boundary. Switching language is host-specific too,
so it hides behind ICultureApplier: AddUIShared
TryAdds the web default that force-loads the server /culture/set endpoint, and UseMauiCulture()
(folded into UseMauiDeviceCapabilities at MMCA.Common.UI.Maui/HostingDependencyInjection.cs:41,
defined at HostingDependencyInjection.cs:128-133) replaces it with
MauiCultureApplier plus the MauiCultureInitializer
startup restore, because a hybrid head has no ASP.NET pipeline and would resolve that URL through the
Blazor router, matching no page and rendering not-found
(MMCA.Common.UI.Maui/Globalization/MauiCultureApplier.cs:8-12). The applier honors only the
allowlisted cultures (MauiCultureApplier.cs:32-35) and persists and activates the culture through
MauiCultureStore before it force-loads the WebView, an order the comment calls
load-bearing (MauiCultureApplier.cs:37-45); the store resolves the startup culture in the same
precedence order the web heads get from request localization, the stored explicit choice, then the
device locale matched by language, then the framework default
(MMCA.Common.UI.Maui/Globalization/MauiCultureStore.cs:31-44), and the initializer applies it
inside MauiAppBuilder.Build(), before any window exists, so the first render is already in the
right language (MMCA.Common.UI.Maui/Globalization/MauiCultureInitializer.cs:14,21-22). That is
ADR-027 and [Rubric §27, i18n]
meeting this group's composition rule.
Wired-but-inert capabilities. A recurring, honest theme in this layer is capabilities that are
fully registered but deliberately do nothing yet, because their real backing requires credentials or
a later feature wave. Native push
(ADR-044,
Website/docs-src/adr/044-native-push-delivery.md) registers a real
IPushRegistrationService
(MMCA.Common.UI/Services/Capabilities/Notifications/IPushRegistrationService.cs:10) on MAUI heads
(MMCA.Common.UI.Maui/DependencyInjection.cs:67), and
MauiPushRegistrationService does the real orchestration, minting a
stable installation id into device preferences and PUTting the installation to the API's
Notifications/Devices endpoint over the authenticated client
(MMCA.Common.UI.Maui/Capabilities/Notifications/MauiPushRegistrationService.cs:22,40-43). What keeps the pipeline
inert is the token end: the IPushDeviceTokenProvider default is
NullPushDeviceTokenProvider, which always yields null
(MMCA.Common.UI/Services/Capabilities/Notifications/NullPushDeviceTokenProvider.cs:12-13), so a head
that does not opt in stays registered-but-tokenless
(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:58-62). The credentialed providers do
exist in the MAUI package behind TFM guards, FcmPushDeviceTokenProvider on Android and
ApnsPushDeviceTokenProvider on iOS/MacCatalyst, registered by AddMauiPushDeviceTokenProvider
(MMCA.Common.UI.Maui/DependencyInjection.cs:118-126), but both are configuration-gated
(Push:Fcm credentials, Push:Apns:Enabled), so a head with no push configuration stays inert even
after calling it (MMCA.Common.UI.Maui/Capabilities/Notifications/FcmPushDeviceTokenProvider.cs:14-16,
MMCA.Common.UI.Maui/Capabilities/Notifications/ApnsPushDeviceTokenProvider.cs:13-15). The
IExternalAuthBroker
(MMCA.Common.UI/Services/Capabilities/Auth/IExternalAuthBroker.cs:10) defaults to
UnavailableExternalAuthBroker so web heads keep their existing
anchor-href OAuth flow (DependencyInjection.cs:64), and
MauiExternalAuthBroker reports IsAvailable == false until the head
configures OAuth:MobileRedirectScheme
(MMCA.Common.UI.Maui/Capabilities/Auth/MauiExternalAuthBroker.cs:35,39), which is also why it is the one
Scoped registration in the native bundle: it navigates through the circuit's NavigationManager
after the system-browser round trip (MMCA.Common.UI.Maui/DependencyInjection.cs:73-76). Media
picking is the same shape read the other way: IMediaPickerService exposes
IsSupported (MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:12) so web heads render
a plain InputFile instead, "the affordance switch, not a degraded path"
(IMediaPickerService.cs:6-7,
ADR-045). Biometrics
stay on their null default until the app-lock wave lands (see the
DevicePreferenceKeys AppLockEnabled key,
MMCA.Common.UI/Services/Capabilities/DeviceStorage/DevicePreferenceKeys.cs:9-10). This "contract present,
behavior inert" pattern is what lets shared components be written against the full capability surface
today while the platform work ships incrementally; each null default is a truthful
IsAvailable == false that hides its affordance rather than a stub that lies.
Device preferences and the per-head lifetime split. IDevicePreferences
(MMCA.Common.UI/Services/Capabilities/DeviceStorage/IDevicePreferences.cs:11) stores per-device settings
(reminder lead time, haptics toggle, app-lock) that describe this device and never roam to the
server, which is why it is distinct from the server-side per-user preferences
(IDevicePreferences.cs:4-9). It exposes an IsPersistent flag (IDevicePreferences.cs:17) so a
head can hide device-settings UI where storage is ephemeral. The three implementations show the
lifetime story clearly: MauiDevicePreferences persists JSON-encoded values
to native Preferences.Default under an mmca.devicePrefs. prefix
(MMCA.Common.UI.Maui/Capabilities/DeviceStorage/MauiDevicePreferences.cs:14,24),
BrowserDevicePreferences persists the same shape to localStorage
through the shared JS module
(MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserDevicePreferences.cs:12,27-28), and
InMemoryDevicePreferences is registered Scoped
(DependencyInjection.cs:83) so the Blazor Server fallback holds per-circuit state in a
ConcurrentDictionary and reports IsPersistent == false
(MMCA.Common.UI/Services/Capabilities/DeviceStorage/InMemoryDevicePreferences.cs:12,15). Never storing
secrets here is a documented rule, tokens belong in platform secure storage
(IDevicePreferences.cs:7), which ties this into [Rubric §26, Front-End Security] and
[Rubric §11, Security].
The native shell pieces that ship beside the contracts. Several members of this group are not
capability contracts at all but the MAUI-side plumbing that ships with them. The token pipeline is
split in two on purpose: MauiSecureTokenStore
(MMCA.Common.UI.Maui/Services/MauiSecureTokenStore.cs:22) is the raw
ISecureTokenStore, holding both tokens in
SecureStorage (Android Keystore, iOS Keychain, Windows DPAPI) with every call guarded so an
OS-invalidated keystore entry degrades to one clean re-login rather than an unhandled throw that
would brick the app on launch (MauiSecureTokenStore.cs:5-21); it writes the refresh token first and
drops both entries on a partial failure so storage is never a mismatched pair
(MauiSecureTokenStore.cs:34-53). MauiTokenStorageService
(MMCA.Common.UI.Maui/Services/MauiTokenStorageService.cs:19) sits above it as the
ITokenStorageService and adds what a
long-lived mobile session needs: a freshness check with a 30-second skew
(MauiTokenStorageService.cs:23,33) and a single-flight refresh under a Lock, because an unguarded
??= lets two callers each start a refresh and every extra refresh rotates the refresh token,
invalidating the pair the other caller still holds (MauiTokenStorageService.cs:38-48). Both halves
are registered Scoped together by AddCommonMauiTokenStorage()
(MMCA.Common.UI.Maui/DependencyInjection.cs:97-101), matching the browser siblings so component code
depends on one lifetime everywhere. MainPageBase
(MMCA.Common.UI.Maui/MainPageBase.cs:20) is the ContentPage base a hybrid head's XAML points at:
it consumes the platform back gesture (MainPageBase.cs:30-35), captures the renderer-scoped
IJSRuntime out of the BlazorWebView through a TaskCompletionSource (MainPageBase.cs:53-62),
and forwards the press to
MauiBackNavigationBridge
(MainPageBase.cs:69), quitting the app only when the WebView history is at its root
(MainPageBase.cs:70-73). MauiPublicLinkBuilder
(MMCA.Common.UI.Maui/Services/MauiPublicLinkBuilder.cs:14) overrides the
IPublicLinkBuilder so share, copy-link and QR
affordances emit the public web URL pinned in PublicSite:BaseUrl rather than the WebView's internal
origin, and it throws at construction when that key is missing
(MauiPublicLinkBuilder.cs:17,28-32). Finally MauiErrorHandlingInitializer
(MMCA.Common.UI.Maui/MauiErrorHandlingInitializer.cs:18), installed by UseMmcaMauiErrorHandling
(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:76-80), hooks the two process-wide last-chance
handlers (AppDomain.UnhandledException and TaskScheduler.UnobservedTaskException) behind a static
guard so a head that calls the extension twice does not report every crash twice
(MauiErrorHandlingInitializer.cs:21-31). These are the [Rubric §11, Security],
[Rubric §25, Navigation & IA] and [Rubric §13, Observability & Operability] concerns that would
otherwise be hand-written in every native head.
Where this group sits. The capability contracts are consumed by the shared Blazor components and
pages (the UI-components group), by the connectivity, battery and accessibility surfaces, and by the
deep-link and notification paths. Nothing here references EF Core, the API, or a message broker
directly, apart from the push registration service's authenticated call to the Devices endpoint: it
is presentation-edge adaptation, sitting alongside the rest of MMCA.Common.UI at the top of the
dependency flow. Read it as the codebase's answer to a specific hard problem, how to write
device-aware UI once and run it on a server, in a browser, and on a phone, with the platform
differences pushed entirely into injected adapters and the shared components none the wiser. The
governing decisions are
ADR-042 (the
abstraction itself, the separate MAUI-TFM package, and the opt-in camera scanner),
ADR-043
(deep links and the native OAuth callback),
ADR-044 (native push delivery),
and ADR-045 (managed
file storage and avatars, the backing for the media-picker capability).
MauiErrorHandlingInitializer
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/MauiErrorHandlingInitializer.cs:18· Level 0 · class (sealed partial)
- What it is: the startup hook that installs the two process-wide last-chance exception handlers for a MAUI head,
AppDomain.UnhandledExceptionandTaskScheduler.UnobservedTaskException, and reports whatever they catch to the app's logger plus an optional crash-reporter callback (MMCA.Common.UI.Maui/MauiErrorHandlingInitializer.cs:5-10,18). A head never constructs it directly: HostingDependencyInjection'sUseMmcaMauiErrorHandling(...)builds and registers it. - Depends on: no first-party types. Externals:
IMauiInitializeService(MAUI hosting) as the contract it implements (MauiErrorHandlingInitializer.cs:18),Microsoft.Extensions.LoggingforILoggerFactory/ILoggerand the[LoggerMessage]source generator (MauiErrorHandlingInitializer.cs:1,127-128), and the BCLSystem.Threading.Locktype plusAppDomainandTaskScheduler(MauiErrorHandlingInitializer.cs:30,70-71). - Concept introduced: the last-chance handler, and why it is an initializer rather than a builder call. Two managed failure modes escape every
trya component can write: an exception thrown on a thread nobody is awaiting (which reachesAppDomain.UnhandledExceptionimmediately before the process dies) and a faultedTasknobody observed (which the finalizer thread re-raises throughTaskScheduler.UnobservedTaskExceptionat the next collection). Hooking them needs anILogger, and the container that can supply one only exists once the MAUI app has been built, so the work cannot happen in the builder extension itself.IMauiInitializeService.Initializeis the first point where the app and its service provider are both available, which is the same reason DeviceCapabilitiesInitializer lives at this layer (MauiErrorHandlingInitializer.cs:11-16).[Rubric §13, Observability & Operability]§13 assesses whether failures become visible signals instead of silence. This turns two categories of otherwise-invisible crash into aCriticallog entry under a dedicated category,MMCA.Common.UI.Maui.UnhandledException(MauiErrorHandlingInitializer.cs:26,127), and gives an app one hook to forward the same event to a real crash reporter.[Rubric §29, Resilience & Business Continuity]§29 assesses graceful behavior at the edges. The unobserved-task path is not just reporting: callingSetObserved()is what keeps a faulted fire-and-forget task from escalating into a process kill at the next collection (MauiErrorHandlingInitializer.cs:100-103).[Rubric §29, Resilience, Reliability & Business Continuity]§29 assesses whether infrastructure concerns are composed once instead of scattered. One registration inMauiProgramcovers the whole process, and no component has to remember anything.
- Walkthrough: two public constants name the source tags handed to the callback,
AppDomainSource="AppDomain"andTaskSchedulerSource="TaskScheduler"(MauiErrorHandlingInitializer.cs:21,24), so a crash reporter can branch on which path fired without string-matching a message. The logger category is a private constant (MauiErrorHandlingInitializer.cs:26). The once-guard is deliberately static:HookSync(aLock) and the_hookedflag (MauiErrorHandlingInitializer.cs:30-31), because the events themselves are process-wide statics and a head that called the builder extension twice would otherwise report every crash twice (MauiErrorHandlingInitializer.cs:28-29). Instance state is only the optional callback_onUnhandled(MauiErrorHandlingInitializer.cs:33) and the resolved_logger(MauiErrorHandlingInitializer.cs:35). The constructor takes the callback and nothing else (MauiErrorHandlingInitializer.cs:42-43).Initialize(IServiceProvider)null-guards its argument, resolves the logger withGetService<ILoggerFactory>()?.CreateLogger(...)rather thanGetRequiredServiceso a head that configured no logging still gets the handlers and the callback (it just has nowhere to write), then callsHookOnce(this)(MauiErrorHandlingInitializer.cs:46-55).HookOncetakes the lock, returns immediately if already hooked, otherwise sets the flag and subscribes both handlers; the write lives in a static method precisely because the state it writes is static (S2696), and the first initializer instance to run owns the handlers for the life of the process (MauiErrorHandlingInitializer.cs:57-73).OnAppDomainUnhandledExceptionreadse.ExceptionObject, which is typed asobjectbecause the runtime can surface a throw that is not a CLR exception at all, and substitutes a stand-inInvalidOperationExceptionso the report shape stays uniform for every callback consumer (MauiErrorHandlingInitializer.cs:75-86).OnUnobservedTaskExceptioncallse.SetObserved()first, ahead of anything that can throw, then reportse.Exceptionor a stand-in (MauiErrorHandlingInitializer.cs:96-107). Both handler bodies wrap everything in a catch-all with CA1031 suppressed and the reason inline: on the last-chance path there is no outer handler left to tell, and the logger may be the very thing that just failed (MauiErrorHandlingInitializer.cs:87-93,108-114).Reportlogs when a logger exists and then invokes the callback (MauiErrorHandlingInitializer.cs:117-125), and the log itself is a source-generated[LoggerMessage]atLogLevel.Critical(MauiErrorHandlingInitializer.cs:127-128), so the hot-path allocation of a formatted message is avoided even here. - Why it's built this way: the ordering rules that govern the rest of this group do not apply to this one call, and the XML doc on
UseMmcaMauiErrorHandlingsays so: because the handlers are installed when the app is built, a logging provider registered after the call is still picked up (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:61-67). The swallow-everything handler bodies are a deliberate trade: a crash reporter that throws on the last-chance path replaces one crash with a worse one (MauiErrorHandlingInitializer.cs:87,108-114). The static once-guard is the only correct shape given that the underlying events are process-wide. - Where it's used: registered as an
IMauiInitializeServicesingleton instance (not a type registration, because the callback has to be captured) byUseMmcaMauiErrorHandling(...)(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:76-80). Both MAUI heads call it with no callback: ADC atMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:113and Store atMMCA.Store/Source/Hosts/UI/MMCA.Store.UI/MauiProgram.cs:74, each with an inline comment restating that the call is ordering-insensitive (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:108-112). - Caveats / not-in-source: this catches managed exceptions only. Anything that never becomes a CLR exception (a native crash such as SIGSEGV, an Objective-C
NSExceptionon iOS, an Android ANR, a stack overflow, or a fail-fast) tears the process down below the runtime and no managed handler runs, so a head needing full crash coverage still pairs this with a platform crash reporter (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:53-60). Not determinable from source: whether any head actually supplies anonUnhandledcallback in a shipped configuration; both call sites read as parameterless today.
BarcodeScanPage
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Media·MMCA.Common.UI.Maui/Capabilities/Media/BarcodeScanPage.cs:21· Level 0 · class (internal sealed partial)
- What it is: the modal scan surface behind MauiBarcodeScannerService: a full-bleed ZXing camera reader with a cancel button underneath, built in C# rather than XAML so the package ships no compiled resource dictionary and the page stays an implementation detail of the service (
MMCA.Common.UI.Maui/Capabilities/Media/BarcodeScanPage.cs:6-9). - Depends on: no first-party types (it is
internal, and only its owning service constructs it). Externals:ZXing.Net.Maui/ZXing.Net.Maui.ControlsforCameraBarcodeReaderView,BarcodeReaderOptionsandBarcodeDetectionEventArgs(BarcodeScanPage.cs:1-2), MAUI'sContentPage,Grid,ButtonandSemanticProperties, and BCLTaskCompletionSource<T>. - Concept introduced: one completion source, many exit paths. A camera scan can end four different ways (a decode, the cancel button, the platform back gesture, or the page simply disappearing), and every one of them must produce exactly one answer for the awaiting caller and must release the camera. The page centralizes that by owning a single
TaskCompletionSource<string?>and routing every exit throughTrySetResult, which is idempotent by construction: the first exit wins and the rest are silently no-ops (BarcodeScanPage.cs:10-14,:72).[Rubric §2, Design Patterns]§2 assesses whether recognized patterns are applied deliberately rather than improvised. This is a promise/completion-source adapter over an event-driven control, the same mechanism MauiSpeechToTextService uses for the recognizer, applied here to a whole navigation surface.[Rubric §21, Accessibility]§21 assesses whether every interactive surface is reachable and describable without sight. Both controls get an explicitSemanticProperties.SetDescription(BarcodeScanPage.cs:43,:51), and the pageTitleis the same camera description (:64), so a screen reader announces what the camera surface is and what the button does.[Rubric §25, Navigation & IA]§25 assesses whether navigation state stays coherent.OnBackButtonPresseddeliberately consumes the gesture instead of letting the platform pop, because the service owns the singlePopModalAsyncand a double pop would remove whatever page came next (BarcodeScanPage.cs:77-80).
- Walkthrough
_completion(BarcodeScanPage.cs:23-24): aTaskCompletionSource<string?>constructed withTaskCreationOptions.RunContinuationsAsynchronously, so the caller's continuation never runs inline on the camera callback thread._reader(BarcodeScanPage.cs:26): theCameraBarcodeReaderViewheld as a field so detection can be stopped later.- The constructor
BarcodeScanPage(string cancelText, string cameraDescription)(BarcodeScanPage.cs:28) builds the reader withFormats = BarcodeFormats.TwoDimensional,AutoRotate = true,Multiple = falseandIsDetecting = true(:30-41). Two-dimensional only is a deliberate accuracy choice: the affordance is a QR/DataMatrix scan, and admitting the 1D formats multiplies false positives on a shaky handheld frame (:34-35). It then subscribesOnBarcodesDetected(:42), sets the reader's semantic description (:43), creates the cancelButtonwith a 16-unit margin and its own semantic description (:45-51), and lays both out in aGridwhose first row isGridLength.Star(the camera) and secondGridLength.Auto(the button) (:53-62).TitleandContentare assigned last (:64-65). Completion(BarcodeScanPage.cs:69): the task the service awaits; it yields the first decoded payload ornullon any cancel.Cancel()(BarcodeScanPage.cs:72):_completion.TrySetResult(null). Safe to call repeatedly and from any exit path, which is what lets the three cancel routes share one method.OnBackButtonPressed()(BarcodeScanPage.cs:75): cancels and returnstrueto consume the gesture (:79-80).OnDisappearing()(BarcodeScanPage.cs:84): stops detection, cancels, then callsbase.OnDisappearing()(:86-88). This is the backstop that guarantees the camera is released even if the page leaves the stack by a route the class did not anticipate.OnCancelClicked(...)(BarcodeScanPage.cs:91): forwards toCancel().OnBarcodesDetected(...)(BarcodeScanPage.cs:93): takes the first result with a non-blankValue(:95), returns without completing when there is none (:96-99), then stops detection before resolving (:103-104), because continued detection would keep raising the event against an already-completed source while the modal animates away (:101-102).StopDetecting()(BarcodeScanPage.cs:107): unsubscribes the handler and setsIsDetecting = false(:109-110).
- Why it's built this way:
partialis not stylistic here. It is required by CsWinRT1028 on the windows TFM because aContentPagecrosses the WinRT ABI, and the redundancy-style rules that object to it on the other three TFMs are silenced project-wide in the csproj (BarcodeScanPage.cs:15-19). Keeping the typeinternalmeans the scan surface is never part of the package's public contract: heads consume the capability through IBarcodeScannerService and can never take a dependency on the page's layout. - Where it's used: constructed exclusively by MauiBarcodeScannerService on the main thread, pushed modally, and popped by the same service (
MMCA.Common.UI.Maui/Capabilities/Media/MauiBarcodeScannerService.cs:90-103).
IFormFactor
MMCA.Common.UI ·
MMCA.Common.UI.Services·MMCA.Common.UI/Services/IFormFactor.cs:7· Level 0 · interface
- What it is: the two-method contract that tells shared UI code which kind of host it is running
in, so a component can adapt without compiling against any host assembly
(
IFormFactor.cs:3-6). - Depends on: nothing first-party, nothing external. Both members return
string. - Concept introduced, host self-description as a resolved service. Everything else in this group
abstracts a device capability (can this head take a photo, speak, vibrate, geocode).
IFormFactorabstracts something coarser and purely descriptive: the identity of the host itself. The distinction matters, because the two answer different questions. A capability contract answers "may I do X here?" and is the one you branch on for behavior;IFormFactoranswers "where am I?" and is the one you use for display and diagnostics. The pattern is the same in both cases though, and it is the pattern this whole group is built on: the shared component library declares an interface, each head registers exactly one implementation at composition time, and no shared code ever testsOperatingSystem.IsAndroid()or reads a#ifsymbol.[Rubric §18, UI Architecture]assesses how presentation concerns are separated across the component tree. Host detection is the classic place that leaks: a singleif (isMaui)inside a shared component pins that component to the set of heads that existed when it was written. Pushing detection behind a resolved interface keeps the component tree host-agnostic and makes "add a fourth head" a composition-root change.[Rubric §1, SOLID]assesses SOLID adherence. This is Dependency Inversion at its smallest: the shared library owns the abstraction, the host owns the implementation, and the dependency arrow points from host to library rather than the reverse.[Rubric §22, Responsive / Cross-Browser]assesses graceful behavior across heads and browsers. One component tree that runs unchanged on Blazor Server, WebAssembly, and MAUI is exactly what this contract makes possible.
- Walkthrough
GetFormFactor()(IFormFactor.cs:10): returns the device form factor. The doc comment gives the expected vocabulary, "Web", "WebAssembly", "Phone" (IFormFactor.cs:9), and the three shipped implementations honor it (see below).GetPlatform()(IFormFactor.cs:13): returns the platform/OS description (IFormFactor.cs:12).- Note what is deliberately absent: no
IsSupportedprobe, no async, noCancellationToken. Both calls are cheap, synchronous, and always answerable, which is why this contract is shaped nothing like the capability contracts registered by DependencyInjection's default table.
- Why it's built this way: ADR-042
(device capability abstraction) is the governing decision for the whole group: per-capability
interfaces in
MMCA.Common.UI, per-head implementations selected at DI composition time. Keeping the interface in the shared UI package (rather than in each app) is what let the three implementations be hoisted out of the consumer apps: each one's XML doc records that it "carries no app-specific state" as the reason it moved (MMCA.Common.UI/Services/WasmFormFactor.cs:5-6,MMCA.Common.UI.Web/Services/WebFormFactor.cs:7-8,MMCA.Common.UI.Maui/Capabilities/MauiFormFactor.cs:7-8). - Where it's used: implemented by WasmFormFactor (WebAssembly),
WebFormFactor (Blazor Server), and MauiFormFactor (native).
Each head registers exactly one of them as a singleton in its composition root, and each registration
helper's doc comment points at the other two so a host author cannot pick the wrong one
(
MMCA.Common.UI/DependencyInjection.cs:180-185,MMCA.Common.UI.Web/DependencyInjection.cs:42-46,MMCA.Common.UI.Maui/DependencyInjection.cs:137-142). The ADC WASM client callsAddWasmFormFactor()(MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web.Client/Program.cs:82), the Blazor Server headAddCommonWebFormFactor()(MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:96), and the MAUI headAddMauiFormFactor()(MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:169). Store wires the same three (MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web.Client/Program.cs:63,MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/Program.cs:132,MMCA.Store/Source/Hosts/UI/MMCA.Store.UI/MauiProgram.cs:102). - Caveats / not-in-source: no shipped
.razorcomponent injectsIFormFactortoday, and no first-party.csfile outside the three implementations, the three registration helpers, the six host composition roots and the two test classes references the interface at all. In the current code its value shows up only through the registrations and their tests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/WasmFormFactorTests.cs:16,MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Services/WebFormFactorTests.cs:20), so treat it as an available extension point rather than a load-bearing one.
IAccessibilityAnnouncer
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Accessibility·MMCA.Common.UI/Services/Capabilities/Accessibility/IAccessibilityAnnouncer.cs:9· Level 0 · interface
- What it is: a one-method contract that pushes a spoken announcement to the platform screen reader for events a sighted user perceives only visually (a live poll opening, a question being answered, the unread badge incrementing).
- Depends on: BCL only (
System.Threading.Tasks.Task,System.Threading.CancellationToken). - Concept introduced, the per-capability contract with per-host adapters. This whole group is a
family of narrow interfaces in
MMCA.Common.UI, each wrapping one device capability so shared Blazor components can call it uniformly while three implementations (MAUI-native, browser-JS-interop, and an inert fallback) are chosen per host at DI composition time (ADR-042).[Rubric §1, SOLID](interface segregation and dependency inversion: components depend on the tiny abstraction, never a platform SDK) and[Rubric §2, Design Patterns](this is the Strategy/adapter plus Null-Object pairing repeated across the group).[Rubric §21, Accessibility]assesses whether non-visual users get equivalent information; this contract routes to MAUISemanticScreenReaderon native and anaria-liveregion in browsers (IAccessibilityAnnouncer.cs:4-7), and is a deliberate silent no-op when no assistive technology is active (IAccessibilityAnnouncer.cs:7). - Walkthrough:
AnnounceAsync(string message, CancellationToken = default)(IAccessibilityAnnouncer.cs:12): announces politely, that is, without interrupting speech already in progress (IAccessibilityAnnouncer.cs:11). - Why it's built this way: a spoken-announcement need has no cross-platform BCL surface, so the capability is inverted behind an interface and satisfied by whichever adapter the host registers; the fallback keeps call sites unconditional (they never branch on "is a screen reader present").
- Where it's used: the framework default is the inert
NullAccessibilityAnnouncer, TryAdd-registered as a singleton inAddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:49); web heads override it withBrowserAccessibilityAnnouncerinAddBrowserDeviceCapabilities(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:99), and the nativeMauiAccessibilityAnnouncerships in theMMCA.Common.UI.Mauipackage. Called by live-update components in the head apps.
MauiBarcodeScannerService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Media·MMCA.Common.UI.Maui/Capabilities/Media/MauiBarcodeScannerService.cs:24· Level 1 · class (sealed)
- What it is: the MAUI camera scanner for IBarcodeScannerService, built on ZXing.Net.MAUI (ADR-042). It pushes BarcodeScanPage modally over the current window page, resolves on the first decoded payload, and pops the page again on every exit path (
MMCA.Common.UI.Maui/Capabilities/Media/MauiBarcodeScannerService.cs:5-11). - Depends on: IBarcodeScannerService (the contract) and BarcodeScanPage (the surface it drives). Externals: MAUI
Application,Page,MainThread,DeviceInfoandDevicePlatform; ZXing.Net.MAUI transitively through the page; BCLFunc<string>andCancellationToken.Register. - Concept introduced: lazily resolved localized text in a singleton. The service is constructed while the app is being built, which is before
MauiCultureInitializerrestores the user's persisted language (ADR-027). Capturing the cancel label and camera description as strings at that moment would pin them to the device language for the life of the process, and no later in-app language switch would ever reach them. The class therefore takesFunc<string>delegates only, and defers the resource lookup to the moment each scan page is built (MauiBarcodeScannerService.cs:16-22,:65-68). This is a general trap worth internalizing: anything captured during host construction predates culture restoration.[Rubric §27, i18n]§27 assesses whether the app follows the user's language everywhere, not just on pages rendered after a switch. There is no string-valued constructor to get this wrong with: the only way to build the service is to hand it the resource lookups themselves, which the XML doc spells out with a() => Localizer["Cancel"]example (MauiBarcodeScannerService.cs:29-35).[Rubric §11, Security]and[Rubric §30, Compliance/Privacy]both bear on the camera permission. The permission belongs to the platform (AndroidCAMERA, iOSNSCameraUsageDescription), and a head that has not declared it, or a user who denies it, gets a scan that simply never decodes and is cancelled out of, which the contract surfaces asnullrather than an exception (MauiBarcodeScannerService.cs:8-11).[Rubric §32, Dependency & Supply-Chain]§32 assesses whether dependencies are carried only where needed. This service is registered byUseCommonBarcodeScanner()alone and is deliberately not folded intoUseMauiDeviceCapabilities(), so a head that never scans ships neither the ZXing camera handler nor a camera permission declaration (MauiBarcodeScannerService.cs:12-15,MMCA.Common.UI.Maui/HostingDependencyInjection.cs:85-87).
- Walkthrough
_cancelText/_cameraDescription(MauiBarcodeScannerService.cs:26-27): the two deferred text resolvers.- The single constructor
MauiBarcodeScannerService(Func<string>, Func<string>)(MauiBarcodeScannerService.cs:36) null-guards both delegates and stores them (:38-42). IsSupported(MauiBarcodeScannerService.cs:51-53):DeviceInfo.Current.Platformequal toDevicePlatform.AndroidorDevicePlatform.iOS. Mac Catalyst and Windows have cameras, but the scan affordance there is a desktop paste field in every head that uses this, and the ZXing camera view is not a supported surface on those targets (:46-50).ScanAsync(CancellationToken = default)(MauiBarcodeScannerService.cs:56): returnsnullimmediately when unsupported or already cancelled (:58-61), otherwise marshals to the main thread and invokes both delegates there, once per scan (:67-68). The whole body sits under acatch-all with an explicit CA1031 suppression whose justification is that scanning is best-effort: a missing window, a denied camera and a handler-less platform must all read as "no scan" (:71-76).ScanOnMainThreadAsync(...)(MauiBarcodeScannerService.cs:79): resolves the host page and returnsnullif there is none (:84-88), constructs the page (:90), pushes it modally (:94), registers the caller's cancellation token againstscanPage.Cancel(:97), awaitsCompletion(:98), and pops the modal in afinallyso the page leaves the stack on every path (:100-103). Every await here usesConfigureAwait(true)on purpose: modal navigation and the camera view are main-thread bound and this method is already on that thread (:92-93).CurrentPage(MauiBarcodeScannerService.cs:106): readsApplication.Current?.Windowsand returnswindows[0].Pagewhen the collection is non-empty (:112-113).Application.MainPageis obsolete on the .NET 10 MAUI train, so the window'sPageis the supported way to reach the active navigation stack (:110-111).
- Why it's built this way: the single
PopModalAsyncliving in the service'sfinally, paired with the page consuming the hardware back gesture, means exactly one pop happens per scan no matter how the scan ended. Combining that with the page's single completion source gives the caller a plainTask<string?>for what is really a four-way race. - Where it's used: registered as a singleton
IBarcodeScannerServicebyUseCommonBarcodeScanner(Func<string>, Func<string>), through a factory that closes over the two delegates (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:104-115). That plainAddSingletononly beats theTryAddSingletondefault of NullBarcodeScannerService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:70) because the head calls it afterAddUIShared(HostingDependencyInjection.cs:97-99). ADC's MAUI head calls it with resource-lookup delegates for badge check-in QR scanning (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:153-155).
MauiSpeechToTextService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Media·MMCA.Common.UI.Maui/Capabilities/Media/MauiSpeechToTextService.cs:14· Level 1 · class (sealed)
- What it is: the MAUI adapter for ISpeechToTextService (ADR-042 Wave 4), driving CommunityToolkit.Maui's
SpeechToTextrecognizer and owning the microphone permission flow (MMCA.Common.UI.Maui/Capabilities/Media/MauiSpeechToTextService.cs:7-13). - Depends on: ISpeechToTextService;
CommunityToolkit.Maui.Media.SpeechToText(MauiSpeechToTextService.cs:2); BCLCultureInfo,IProgress<string>andTaskCompletionSource<T>. - Concept introduced: bridging an event-driven recognizer to a single awaitable call. The toolkit exposes start/stop plus "result updated" and "result completed" events; the contract wants one
ListenAsyncthat returns the final text. ATaskCompletionSource<string?>resolved from the completion handler is the idiomatic adapter, the same shape BarcodeScanPage uses for its four exit paths.[Rubric §21, Accessibility]and[Rubric §24, Forms/Validation/UX Safety]: dictation is an input affordance, so a permission denial or a recognizer failure must never wedge a form. Every negative outcome returnsnulland the affordance simply does nothing.
- Walkthrough
IsSupported(MauiSpeechToTextService.cs:17):true.ListenAsync(CultureInfo culture, IProgress<string>? partialResults, CancellationToken = default)(MauiSpeechToTextService.cs:20): null-guardsculture(:25); requests recognition permissions and returnsnullwhen denied (:29-32); creates the completion source withRunContinuationsAsynchronously(:34); declaresOnUpdatedto forward interim text topartialResults(:36-37) andOnCompletedto resolve the finalTextwhen the recognition result is successful andnullotherwise (:39-40); subscribes both (:42-43); starts listening withSpeechToTextOptionscarrying the culture andShouldReportPartialResultsset only when a progress sink was supplied (:46-51); registers the caller's cancellation to resolvenull(:53-54); and awaits the completion (:55). Thefinallyunsubscribes both handlers and callsStopListenAsync(CancellationToken.None)(:57-62).OperationCanceledExceptionreturnsnull(:64-67), as does theInvalidOperationExceptionorFeatureNotSupportedExceptionpair matched by an exception filter (:68-71).
- Why it's built this way: the guaranteed unsubscribe-and-stop in
finallyis what prevents a leaked recognizer session across dictations, and stopping withCancellationToken.Nonemeans a cancelled listen still shuts the microphone down rather than abandoning it. Reporting partial results only when the caller passed anIProgress<string>avoids paying for interim recognition events nobody consumes. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:62); the fallback is NullSpeechToTextService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:54). Consumed by dictation affordances on text inputs. - Caveats / not-in-source: heads must chain
.UseMauiCommunityToolkit()onto their ownUseMauiApp<T>()call for this adapter to work; the toolkit's MCT001 analyzer requires that call to appear in the app's own builder chain, soUseMauiDeviceCapabilities()cannot make it on the head's behalf (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:23-28).
MauiTextToSpeechService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Media·MMCA.Common.UI.Maui/Capabilities/Media/MauiTextToSpeechService.cs:12· Level 1 · class (sealed partial,IDisposable)
- What it is: the MAUI adapter for
ITextToSpeechService, speaking text overTextToSpeech.Defaultwith locale matching and a cancellable in-flight utterance. - Depends on:
ITextToSpeechService; MAUI EssentialsTextToSpeech,SpeechOptions,Locale; BCLLock,CancellationTokenSource,CultureInfo. - Concept: single-utterance serialization plus best-effort locale selection.
[Rubric §21, Accessibility]assesses assistive affordances; read-aloud is one, and it is offered as a first-class capability rather than a platform afterthought.[Rubric §27, i18n]assesses culture-awareness; the adapter picks a voice for the current UI culture and falls back to the platform default, so a device without anesvoice still speaks rather than throwing (MMCA.Common.UI.Maui/Capabilities/Media/MauiTextToSpeechService.cs:7-9). - Walkthrough
_gate(MauiTextToSpeechService.cs:14, aLock) and_activeUtterance(MauiTextToSpeechService.cs:15, a nullableCancellationTokenSource) track the one in-flight utterance.IsSupported(MauiTextToSpeechService.cs:18): a constanttrue.SpeakAsync(string text, CancellationToken = default)(MauiTextToSpeechService.cs:21): guardstextwithArgumentException.ThrowIfNullOrWhiteSpace(:23), callsStopAsyncfirst so a new utterance preempts the previous one (:25), links a fresh CTS to the caller's token and stores it under the lock (:27-31), then speaks withSpeechOptionswhoseLocalecomes fromMatchLocaleAsync(CultureInfo.CurrentUICulture)(:35-39).OperationCanceledExceptionis expected and swallowed (:41-44); thefinallyclears_activeUtteranceonly if it is still this utterance, then disposes the CTS (:45-56).StopAsync()(MauiTextToSpeechService.cs:60): reads the active CTS under the lock, returns if there is none (:68-71), elseCancelAsync, swallowingObjectDisposedExceptionfor the case where the utterance completed concurrently (:77-80).Dispose()(MauiTextToSpeechService.cs:84): disposes and clears any active CTS under the lock.MatchLocaleAsync(CultureInfo culture)(MauiTextToSpeechService.cs:93): fetches the installed locales and returns the first whoseLanguagematches the culture's two-letter ISO code (:97-99), ornull(meaning "platform default voice") on no match or onFeatureNotSupportedException(:101-104).
- Why it's built this way: MAUI exposes no stop API, so
StopAsynccancels the in-flight utterance's token instead (MauiTextToSpeechService.cs:10). TheLock-guarded single-utterance state keeps overlappingSpeakAsynccalls from talking over each other, and returningnullfrom locale matching lets the platform choose a voice rather than failing the whole call. The per-head selection itself is ADR-042. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:55), overriding theNullTextToSpeechServicedefault that the sharedDependencyInjectionTryAdds atMMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:48. Consumed by read-aloud affordances in shared components.
WasmFormFactor
MMCA.Common.UI ·
MMCA.Common.UI.Services·MMCA.Common.UI/Services/WasmFormFactor.cs:9· Level 1 · class (sealed)
- What it is: the IFormFactor implementation for the WebAssembly head. It
reports the literal string
"WebAssembly"and the browser-reported OS description. - Depends on: implements IFormFactor; BCL
Environment.OSVersiononly. No JS interop, no MAUI Essentials, nothing that could fail during prerender. - Concept: none new. This is the WASM member of the three-implementation family introduced by
IFormFactor; its siblings are WebFormFactor and
MauiFormFactor, and the class doc names them explicitly
(
WasmFormFactor.cs:6-7). Worth noting the contrast with the null-object fallbacks elsewhere in this chapter (see NullGeocodingService): there is no "null form factor", because unlike a capability there is no such thing as a host that does not know what it is, so every head must register a real implementation and none is registered by default. - Walkthrough
sealed class WasmFormFactor : IFormFactor(WasmFormFactor.cs:9). Sealed and stateless: no fields, no constructor, so the singleton lifetime its registration uses costs nothing and is safe to share across every circuit and component in the browser runtime.GetFormFactor()(WasmFormFactor.cs:12): a constant"WebAssembly". The class doc explains why a constant is correct rather than a probe: this code only ever executes after the WASM runtime has loaded in the browser, so the answer cannot vary (WasmFormFactor.cs:4-5).GetPlatform()(WasmFormFactor.cs:15):Environment.OSVersion.ToString(). Under WASM this is the browser-reported OS description, not the server's, which is precisely the difference from WebFormFactor: the identically-written line (MMCA.Common.UI.Web/Services/WebFormFactor.cs:18) returns the server OS because it runs on the server. Two implementations that differ only in where they execute is the cleanest possible illustration of why this contract is resolved rather than computed.
- Why it's built this way: ADR-042.
It lives in
MMCA.Common.UIrather than in a WASM-specific package because it needs no WASM-specific reference: BCL only. That is what makes the registration helperAddWasmFormFactor()a plain singleton registration in the shared package (MMCA.Common.UI/DependencyInjection.cs:186-187), and the surrounding doc comment is where the three-way choice is documented for host authors (MMCA.Common.UI/DependencyInjection.cs:180-185). - Where it's used: registered by
AddWasmFormFactor()(MMCA.Common.UI/DependencyInjection.cs:186) from the.ClientWASM host only (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web.Client/Program.cs:82,MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web.Client/Program.cs:63). Covered byWasmFormFactorTests, which asserts the singleton lifetime, that exactly oneIFormFactordescriptor is present, and that the two strings come back as documented (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/WasmFormFactorTests.cs:16-28,31-37).
NullAccessibilityAnnouncer
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Accessibility·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Accessibility/NullAccessibilityAnnouncer.cs:4· Level 1 · class (sealed)
- What it is: the default
IAccessibilityAnnouncer: screen-reader announcements are accepted and dropped (NullAccessibilityAnnouncer.cs:3). - Depends on:
IAccessibilityAnnouncer; BCLTask. - Concept: the same neutral-default shape taught under
AlwaysOnlineConnectivityStatusService, in its smallest possible form. Note there is noIsSupportedprobe on this contract, so a component cannot branch on availability and does not need to: announcing is fire-and-forget by design.[Rubric §21, Accessibility]assesses whether non-visual users receive the information sighted users get. This default is the absence of that channel, which is why both real heads implement it:BrowserAccessibilityAnnouncerwrites into anaria-liveregion andMauiAccessibilityAnnouncerpushes to the OS screen reader. A head that keeps this default silently loses live-region announcements.
- Walkthrough: one member.
AnnounceAsync(string message, CancellationToken)(NullAccessibilityAnnouncer.cs:7) ignores both arguments and returnsTask.CompletedTask. - Why it's built this way: ADR-042. Call sites stay unconditional: a component announces a change without first checking whether an assistive channel exists, and the container decides whether that announcement goes anywhere.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:58); overridden on web heads (DependencyInjection.cs:108) and on native heads (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:56), so in practice only a head that calls neither override keeps it. Asserted non-throwing inCapabilityFallbackTests(CapabilityFallbackTests.cs:161).
MainPageBase
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/MainPageBase.cs:20· Level 2 · class (abstract)
- What it is: the base
ContentPagefor a MAUI Blazor Hybrid head whose XAML hosts a singleBlazorWebView. It intercepts the platform back gesture (Android hardware back, iOS swipe) and forwards it into the WebView's own history stack, quitting the app only when the WebView has nowhere left to go (MMCA.Common.UI.Maui/MainPageBase.cs:7-11,20). - Depends on: first-party, MauiBackNavigationBridge and its BackNavigationResult return type, imported from
MMCA.Common.UI.Services.Navigation(MainPageBase.cs:3,69-70). Externals:ContentPage,MainThreadandApplication.Current(MAUI),Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView(MainPageBase.cs:1), andMicrosoft.JSInterop.IJSRuntime(MainPageBase.cs:2). - Concept introduced: a native gesture routed into web history. A hybrid head has two navigation stacks that know nothing about each other: the native page stack and the WebView's
history. Without this base, Android's back button pops the native stack, and since a hybrid head has exactly one page, the app exits no matter how deep the Blazor router has navigated. This type turns the native gesture into a question asked of the web stack first, and treats "exit the app" as the answer of last resort.[Rubric §25, Navigation & IA]§25 assesses whether navigation intent is modeled coherently across every entry point. Funnelling the hardware gesture through the same history the Blazor router drives keeps one navigation model instead of two competing ones.[Rubric §18, UI Architecture]§18 assesses how much host-specific plumbing leaks into app code. Because the base owns the whole interception, a head adopts it in two edits: point the XAML root element at this type, and overrideHostWebViewto return thex:Named control (MainPageBase.cs:12-18).[Rubric §29, Resilience & Business Continuity]§29 assesses graceful behavior at edge states. Every failure path here (WebView not hydrated, dispatch refused, interop threw) ends in a clean quit rather than a swallowed gesture or an unhandled exception.
- Walkthrough:
HostWebViewis an abstract protected property (MainPageBase.cs:27): the XAML-generatedx:Namefield is private to the derived partial class, so the base can only reach the control through an override (MainPageBase.cs:22-26).OnBackButtonPressed()startsHandleBackAsync()without awaiting it and returnstrue, which consumes the gesture immediately and moves the decision off the UI thread (MainPageBase.cs:30-35).HandleBackAsync()then bridges a synchronous API to async work:BlazorWebViewonly exposes theAction<IServiceProvider>dispatch overload, so the method creates aTaskCompletionSource<IJSRuntime?>(MainPageBase.cs:53), callsHostWebView.TryDispatchAsync(...)with the tinyCaptureJsRuntimecallback that resolves the renderer-scopedIJSRuntimeinto that source (MainPageBase.cs:37-38,54), and awaits the task outside the dispatch context (MainPageBase.cs:62). Two guards quit early: dispatch refused (MainPageBase.cs:56-60) and a nullIJSRuntime(MainPageBase.cs:63-67). Otherwise it delegates toMauiBackNavigationBridge.HandleBackPressedAsync(jsRuntime)and quits only when the returned result reportsAtRoot(MainPageBase.cs:69-73). Quitting is a two-hop helper:QuitApp()marshals back withMainThread.BeginInvokeOnMainThread(MainPageBase.cs:40-41) andQuitOnMainThread()callsApplication.Current?.Quit()(MainPageBase.cs:43-44). A deliberate catch-all wraps the whole body with CA1031 suppressed and the reason inline, that the interop failure modes differ per platform and none of them are recoverable here, degrading to a clean exit (MainPageBase.cs:75-81). - Why it's built this way: the bridge itself lives in
MMCA.Common.UIso the JS interop module ships with the shared UI package, and this page is the thin native adapter over it. The bridge reportsHandled/AtRootrather than navigating on its own, precisely so the native side decides what "no history left" means. Owning no XAML of its own keeps the base adoptable by any head regardless of what that head's page declares: the class doc spells out the two-edit adoption, anxmlns:mauiroot element swap plus theHostWebViewoverride (MainPageBase.cs:12-18). This is the same host-adapter shape as the rest of the group (ADR-042), applied to a navigation gesture rather than a device API. - Where it's used: both MAUI heads derive from it. ADC names the base in code-behind (
MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MainPage.xaml.cs:12,17), and Store's code-behind declares no base at all because the XAML root element supplies it, leaving only theHostWebViewoverride (MMCA.Store/Source/Hosts/UI/MMCA.Store.UI/MainPage.xaml.cs:10,16). - Caveats / not-in-source: whether a given gesture reaches
OnBackButtonPressedat all is platform behavior, not visible here (the class doc states Android hardware back and the iOS swipe,MainPageBase.cs:9-11). Not determinable from source in this unit: the JavaScript helper that actually inspects the history stack, which is a JS asset shipped with MMCA.Common.UI rather than C#.
MauiMediaPickerService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Media·MMCA.Common.UI.Maui/Capabilities/Media/MauiMediaPickerService.cs:11· Level 2 · class (sealed)
- What it is: the MAUI adapter for
IMediaPickerService: it opens the platform photo library or camera for avatar upload and returns the result as aPickedMedia, ornull. - Depends on:
IMediaPickerServiceandPickedMedia; MAUI EssentialsMediaPicker,DeviceInfo,FileResult. - Concept introduced, stream-ownership-safe cancellation. The single most instructive detail in this class is where the cancellation check sits, and the code explains itself in a four-line comment (
MauiMediaPickerService.cs:38-41). The token is checked after the file is picked but before the stream is opened. If it were checked afterOpenReadAsync, throwing would leak the file handle, because at that instant nothing downstream owns anything disposable yet: thePickedMediathat takes ownership of the stream has not been constructed. A cancellation that lands duringOpenReadAsynctherefore just returns the picked media, which the caller disposes as usual. This is a good model for placing a cancellation check in any acquire-then-wrap sequence.[Rubric §12, Performance & Scalability]assesses resource handling. The ownership boundary here is explicit: either no stream is opened, or aPickedMediaexists to dispose it.[Rubric §15, Best Practices & Code Quality]assesses disciplined suppressions. Both#pragma warning disableblocks are narrowly scoped and carry an inline justification:CS0618becausePickPhotoAsyncis obsolete only in favor of a multi-select API and an avatar is exactly one photo (:18-20), andCA1031because picking is best-effort and a denied permission must becomenull(:51-53).
- Walkthrough
IsSupported(MauiMediaPickerService.cs:14): the one non-constantIsSupportedamong the native adapters in this unit,MediaPicker.Default.IsCaptureSupported || DeviceInfo.Current.Platform != DevicePlatform.WinUI. In words: any non-WinUI platform is supported, and WinUI is supported only if the platform reports camera capture.PickPhotoAsync(CancellationToken = default)(MauiMediaPickerService.cs:17): delegates straight to the shared core withMediaPicker.Default.PickPhotoAsync()as the picking function (:19).CapturePhotoAsync(CancellationToken = default)(MauiMediaPickerService.cs:23): checksMediaPicker.Default.IsCaptureSupportedfirst and short-circuits toTask.FromResult<PickedMedia?>(null)on a device with no camera (:24-26), otherwise runs the same core with the capture function.PickCoreAsync(Func<Task<FileResult?>> pick, CancellationToken)(MauiMediaPickerService.cs:28): the shared body. It invokes the supplied picker (:32), returnsnullwhen the user cancelled the sheet (:33-36), performs the ownership-safe cancellation check (:42), opens the stream (:44), and wraps it in aPickedMediawith the file name and a content type defaulted toapplication/octet-streamwhen the platform reports none (:45).- The catch order is load-bearing:
OperationCanceledExceptionis re-thrown (:47-50) so cancellation stays cancellation, and only then does the broad catch turn everything else intonull(:52-56).
- Why it's built this way: ADR-045 for avatars and managed file storage, ADR-042 for the layer. Web heads do not need a native picker at all (they render an
InputFile), so the entire native flow, including its permission prompts, lives here behind the capability contract and never reaches the shared component. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:71), overridingNullMediaPickerService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:65). The registration comment records the out-of-code prerequisite: the head must declare the camera permission itself, AndroidCAMERAplus the iOS usage strings (MMCA.Common.UI.Maui/DependencyInjection.cs:69-70). - Caveats / not-in-source:
MediaPickerowns the platform permission prompts (MauiMediaPickerService.cs:6-7), so no permission code appears in this class; a denied permission is one of the failures the broad catch turns intonull.
BrowserAccessibilityAnnouncer
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Accessibility·MMCA.Common.UI/Services/Capabilities/Accessibility/BrowserAccessibilityAnnouncer.cs:8· Level 2 · class (sealed)
- What it is: the web adapter for
IAccessibilityAnnouncer. Where the native head calls the OS screen-reader API directly, the browser has no such API, so this writes the message into a visually hiddenaria-live="polite"region that every screen reader already monitors. - Depends on:
IAccessibilityAnnouncerandCapabilitiesJsModule(constructor-injected,MMCA.Common.UI/Services/Capabilities/Accessibility/BrowserAccessibilityAnnouncer.cs:10,:13); theannounceexport ofcapabilities-interop.js(MMCA.Common.UI/wwwroot/capabilities-interop.js:73). - Concept introduced: the live region as the web's equivalent of a screen-reader announce call. The JS side (
MMCA.Common.UI/wwwroot/capabilities-interop.js:52-71) creates onedivon first use witharia-live="polite"androle="status"(:57-58), styled inline (absolute, 1px,clip-path: inset(50%),:59-67) so it is invisible but not hidden from assistive tech, and appended todocument.body(:68).announceclears the region's text before setting it after a 50 ms timeout (:77-80) so that repeating the same message is re-announced rather than ignored as an unchanged node.[Rubric §21, Accessibility]assesses whether non-visual users receive information a sighted user gets from a purely visual change; this is the single mechanism the whole web head uses for that.[Rubric §18, UI Architecture]applies because the live region is created by the capability layer rather than by each page's markup, so no component has to remember to render one. - Walkthrough: the constructor captures the shared module into
_module(MMCA.Common.UI/Services/Capabilities/Accessibility/BrowserAccessibilityAnnouncer.cs:10,:13).AnnounceAsync(string message, CancellationToken = default)(:16-19) is one awaitedInvokeOrDefaultAsync<bool?>("announce", [message], cancellationToken)whose result is discarded: the contract returnsTask, and there is no useful caller response to "the announcement did not land". - Why it's built this way: routing through
CapabilitiesJsModulemeans an announcement fired during prerender (before any DOM exists) is a silent no-op instead of an exception, which matters because announcements are typically triggered from lifecycle methods that also run server-side. Creating the region lazily in JS keeps the RCL free of any required markup or stylesheet (MMCA.Common.UI/wwwroot/capabilities-interop.js:47-48). - Where it's used: registered scoped as
IAccessibilityAnnouncerbyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:99); siblings areMauiAccessibilityAnnouncerandNullAccessibilityAnnouncer. Consumed by components announcing live updates.
DependencyInjection
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/DependencyInjection.cs:32· Level 3 · class (static)
- What it is: the service-level registration surface for the MAUI native device-capability layer (ADR-042). It binds every capability contract the framework backs natively to its MAUI implementation (
AddMauiDeviceCapabilities()), and adds four deliberately separate opt-ins: the secure-enclave token pipeline (AddCommonMauiTokenStorage()), the platform push-token provider (AddMauiPushDeviceTokenProvider()), the public-URL link builder (AddCommonMauiPublicLinkBuilder()), and the native IFormFactor (AddMauiFormFactor()). - Depends on: first-party, the capability contract set in
MMCA.Common.UI.Services.Capabilitieswith theirMaui*bodies inMMCA.Common.UI.Maui.Capabilities, plusMMCA.Common.UI.Maui.Services,MMCA.Common.UI.ServicesandMMCA.Common.UI.Services.Authfor IFormFactor, IPublicLinkBuilder, ISecureTokenStore and ITokenStorageService (MMCA.Common.UI.Maui/DependencyInjection.cs:1-20). Externals:IServiceCollectionas the extended type. - Concept introduced:
extension(IServiceCollection)registration blocks, and lifetime choice as documented intent. The class isstatic(DependencyInjection.cs:32) and every member lives inside anextension(IServiceCollection services)block (DependencyInjection.cs:34), the C# preview extension-member syntax this codebase uses for DI registration everywhere (see the primer); the methods appear as instance methods onIServiceCollectionat the call site. Two lifetimes appear here and the code explains both: singleton for the capability services, because a MAUI head is single-user and the stateful ones (connectivity, battery) wrap app-global platform events (DependencyInjection.cs:44-45), and scoped for IExternalAuthBroker, which navigates through the circuit'sNavigationManagerafter a system-browser round trip (DependencyInjection.cs:73-75), and for the token pipeline so component code sees one lifetime across every head (DependencyInjection.cs:92-95).[Rubric §1, SOLID]§1 assesses dependency inversion in practice. Every consumer depends on a capability interface and never on a MAUI type, which is what makes the browser and fallback adapters in this group drop-in substitutes.[Rubric §29, Resilience, Reliability & Business Continuity]§29 assesses whether infrastructure concerns are composed in one place instead of scattered. A single extension method carries the entire native binding set, so a head'sMauiProgramstays short and no capability can be silently forgotten.[Rubric §11, Security]§11 assesses how credentials are stored and handled. The token half puts both tokens in the platform secure enclave and guards every read and write, so an OS-invalidated keystore entry degrades to one clean re-login rather than an unhandled throw on launch (DependencyInjection.cs:81-86).
- Walkthrough:
AddMauiDeviceCapabilities()(DependencyInjection.cs:42) registers seventeen capability contracts as singletons in one block (DependencyInjection.cs:46-62): connectivity, battery, share, clipboard, haptics, map navigation, geolocation, geocoding, external links, text-to-speech, accessibility announcer, local notifications, screenshot, device preferences, local cache, biometrics, and speech-to-text. Three further registrations carry conditions and are commented for it. IPushRegistrationService is bound to MauiPushRegistrationService but yields nothing until the app registers a credentialed IPushDeviceTokenProvider, so it is wired-but-inert out of the box (ADR-044,DependencyInjection.cs:64-67). IMediaPickerService is bound to MauiMediaPickerService, whose capture path prompts for the camera permission the head must declare, AndroidCAMERAplus the iOS usage strings (ADR-045,DependencyInjection.cs:69-71). IExternalAuthBroker isAddScopedto MauiExternalAuthBroker and stays inert (IsAvailable == false) until the head configuresOAuth:MobileRedirectSchemeand registers the platform callback (DependencyInjection.cs:73-76). The method returnsservicesfor chaining (DependencyInjection.cs:77).AddCommonMauiTokenStorage()registers the pipeline in two scoped halves: MauiSecureTokenStore asISecureTokenStoreand MauiTokenStorageService asITokenStorageServiceon top of it, the latter checking expiry and refreshing proactively instead of handing callers a stale bearer (DependencyInjection.cs:97-101). Both halves are required, and the split is what keeps the graph acyclic: storage depends on the refresher, and the refresher depends on the raw store rather than back on storage (DependencyInjection.cs:87-90).AddMauiPushDeviceTokenProvider()is the one compile-time-conditional member: under#if ANDROIDit registersFcmPushDeviceTokenProvider, underIOS || MACCATALYSTit registersApnsPushDeviceTokenProvider, and the windows TFM registers nothing at all so the pipeline stays inert there (DependencyInjection.cs:118-126). Both providers are additionally configuration-gated onPush:Fcmcredentials andPush:Apns:Enabled(DependencyInjection.cs:109-115).AddCommonMauiPublicLinkBuilder()binds MauiPublicLinkBuilder as the scopedIPublicLinkBuilder, so share, copy-link and QR affordances emit the public web URL fromPublicSite:BaseUrlinstead of the WebView's internal origin (DependencyInjection.cs:128-135).AddMauiFormFactor()binds IFormFactor to MauiFormFactor as a singleton, kept separate fromAddMauiDeviceCapabilities()so heads that still register their own implementation keep last-registration-wins control (DependencyInjection.cs:137-144). - Why it's built this way: the class doc is explicit that these are plain
Addcalls (notTryAdd) and must run afterAddUIShared, so the native bodies override the shared fallback defaults under last-registration-wins (DependencyInjection.cs:25-31). That is the whole selection mechanism of ADR-042: the shared package alwaysTryAdd-registers an inert default so the DI graph resolves on every host, and a native head simply overwrites the entries it can do better. Splitting token storage, push tokens, the link builder and form factor into their own methods preserves that same override control per concern for a head that wants its own implementation. Token storage is scoped rather than singleton purely to match its browser siblings,AddCommonServerTokenStorage()in MMCA.Common.UI.Web and the WASM WasmTokenStorageService, so component code depends on one lifetime everywhere (DependencyInjection.cs:92-95). - Where it's used:
AddMauiDeviceCapabilities()is called by HostingDependencyInjection'sUseMauiDeviceCapabilities()(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:33), the builder-level entry point the class doc steers heads toward (DependencyInjection.cs:26-28). The four opt-ins are called directly by each head: ADC calls all four (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:122,143,163,169), Store calls only token storage and form factor (MMCA.Store/Source/Hosts/UI/MMCA.Store.UI/MauiProgram.cs:96,102). - Caveats / not-in-source: "wired but inert" is a real runtime state for push registration, media capture, external auth, and the push-token providers. Registration does not imply the capability works without the extra host configuration each comment names. The platform wiring the push providers depend on stays app-side: the Android
POST_NOTIFICATIONSdeclaration and credentials, and on iOS theaps-environmententitlement plus the two AppDelegate callbacks that publish intoApnsTokenBridge(DependencyInjection.cs:112-115). Not determinable from source in this unit: the bodies of the individualMaui*Serviceimplementations, which the other units of this group cover.
DeviceCapabilitiesInitializer
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:14· Level 3 · class (sealed)
- What it is: a MAUI startup hook that bridges local-notification taps into Blazor routing. It implements
IMauiInitializeService, so itsInitializeruns while the MAUI app is being built, and it forwards the route carried by a tapped reminder to the shared IDeepLinkDispatcher (MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:7-14). - Depends on: first-party, IDeepLinkDispatcher from
MMCA.Common.UI.Services.Capabilities.Navigation(DeviceCapabilitiesInitializer.cs:1,21). Externals:IMauiInitializeService(MAUI hosting), andPlugin.LocalNotificationwith itsNotificationActionEventArgs(DeviceCapabilitiesInitializer.cs:2-3). - Concept introduced: cold-start deep-link buffering. This type is the native publisher end of the deep-link funnel; the receiver end is the shared
DeepLinkListenercomponent rendered in the layout. A tap that arrives while the app is running travels live through IDeepLinkDispatcher; a tap that cold-starts the process arrives before the Blazor router exists, so the dispatcher buffers the pending route until first render and the listener drains it (DeviceCapabilitiesInitializer.cs:8-12).[Rubric §25, Navigation & IA]§25 assesses how navigation intent flows through the app. Routing every native entry point through one dispatcher keeps the Blazor router the single source of truth for where the user lands.[Rubric §29, Resilience & Business Continuity]§29 assesses graceful handling of edge states. The cold-start buffer is what stops a tap that launched the process from being lost before there is anything to navigate.
- Walkthrough: the class is
sealed(DeviceCapabilitiesInitializer.cs:14).Initialize(IServiceProvider services)null-guards its argument (DeviceCapabilitiesInitializer.cs:17-19), then resolves IDeepLinkDispatcher withGetServicerather thanGetRequiredServiceand returns early when none is registered (DeviceCapabilitiesInitializer.cs:21-25), so a head without the dispatcher gets a no-op instead of a startup crash. When one is present it subscribes toLocalNotificationCenter.Current.NotificationActionTappedwith a lambda closing over the resolved dispatcher (DeviceCapabilitiesInitializer.cs:27). The static handlerOnNotificationTapped(DeviceCapabilitiesInitializer.cs:30) ignores dismissals (DeviceCapabilitiesInitializer.cs:32-35), reads the app-relative route fromargs.Request?.ReturningData(DeviceCapabilitiesInitializer.cs:37), and publishes only when that route is non-blank (DeviceCapabilitiesInitializer.cs:38-41). - Why it's built this way: notification metadata is not routing. Translating the plugin's tap event into this codebase's own IDeepLinkDispatcher vocabulary here means the shared listener component never references
Plugin.LocalNotification, so the same component works on hosts that have no notification plugin at all (ADR-042). Wiring it as anIMauiInitializeServiceestablishes the subscription exactly once, at app build time, before any UI renders. The defensive early return is what makes the hook safe to register unconditionally, which is why HostingDependencyInjection can add it with no condition. - Where it's used: registered by HostingDependencyInjection as an
IMauiInitializeServicesingleton insideUseMauiDeviceCapabilities()(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:34); the routes it publishes are consumed through IDeepLinkDispatcher (implemented by DeepLinkDispatcher) by the sharedDeepLinkListenercomponent that ADC registers with its MAUI-only UI module (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:157-159). - Caveats / not-in-source: the route contract is entirely
ReturningDataon the scheduled notification, so a reminder created without an app-relative route in that field produces no navigation. Not determinable from source in this unit: theDeepLinkListenercomponent body and the scheduling code that populatesReturningData, both outside this unit.
HostingDependencyInjection
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/HostingDependencyInjection.cs:15· Level 4 · class (static)
- What it is: the
MauiAppBuilder-level entry point for the device-capability layer (ADR-042), the hybrid culture wiring (ADR-027), and the process-wide unhandled-exception hook. Four builder extensions:UseMauiDeviceCapabilities()composes the service registrations plus the platform hooks that need the builder itself,UseMmcaMauiErrorHandling(...)installs the last-chance handlers,UseCommonBarcodeScanner(...)is the opt-in camera-scanning add-on, andUseMauiCulture()is the separately callable culture half (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:10-15). - Depends on: first-party, DependencyInjection's
AddMauiDeviceCapabilities()(HostingDependencyInjection.cs:33), DeviceCapabilitiesInitializer (HostingDependencyInjection.cs:34), MauiErrorHandlingInitializer (HostingDependencyInjection.cs:78), MauiBarcodeScannerService behind IBarcodeScannerService (HostingDependencyInjection.cs:112-113), and the globalization pair MauiCultureApplier / MauiCultureInitializer behind ICultureApplier (HostingDependencyInjection.cs:2,130-131). Externals:MauiAppBuilderas the extended type,Plugin.LocalNotification'sUseLocalNotification()(HostingDependencyInjection.cs:5,32), andZXing.Net.Maui.Controls'UseBarcodeReader()(HostingDependencyInjection.cs:6,111). - Concept introduced: builder-level versus service-level composition. This is the layered pairing that runs through MAUI hosting: DependencyInjection registers services on
IServiceCollection, while this class operates onMauiAppBuilderbecause some steps (the Plugin.LocalNotification lifecycle wiring, the ZXing handler registration, theIMauiInitializeServicehooks) need more than a service collection. It uses the sameextension(MauiAppBuilder builder)block syntax (HostingDependencyInjection.cs:17). The second concept, visible inUseCommonBarcodeScanner, is lazy text resolution: everything in this class runs while the app is being built, which is before MauiCultureInitializer restores the user's persisted language, so any string captured eagerly here would be pinned to the startup device language for the life of the process. TakingFunc<string>delegates instead, invoked once per scan when the page is built, is what makes the modal follow the in-app language switch (HostingDependencyInjection.cs:88-95).[Rubric §15, Best Practices & Code Quality]§15 assesses how hard the framework is to adopt correctly. Folding four easy-to-forget steps into one fluent call removes most of the ways a head can be left half-configured.[Rubric §33, Developer Experience]§33 assesses the ceremony a developer pays for a working host. One builder extension plus a small set of documented obligations is that ceremony, and the obligations the wrapper cannot absorb are spelled out in XML docs instead of failing silently.[Rubric §27, i18n]§27 assesses whether localization reaches every surface. TheFunc<string>parameters exist precisely so the scan page follows the in-app language switch rather than the startup device language, and the culture fold-in guarantees a hybrid head has a working applier at all.[Rubric §13, Observability & Operability]§13 assesses whether failures become visible.UseMmcaMauiErrorHandlingis the one line that makes a background-thread throw land in the app's logger atCriticalunder a dedicated category instead of vanishing (HostingDependencyInjection.cs:45-52).
- Walkthrough:
UseMauiDeviceCapabilities()(HostingDependencyInjection.cs:30) does four things in order:builder.UseLocalNotification()initializes the notification plugin (:32);builder.Services.AddMauiDeviceCapabilities()binds every native capability (:33);AddSingleton<IMauiInitializeService, DeviceCapabilitiesInitializer>()registers the notification-tap deep-link bridge (:34); andbuilder.UseMauiCulture()folds in the hybrid culture wiring (:41). It returnsbuilderfor chaining (:42).UseMmcaMauiErrorHandling(Action<Exception, string>? onUnhandled = null)(HostingDependencyInjection.cs:76-80) is a single registration: anIMauiInitializeServicesingleton built from a constructed instance of MauiErrorHandlingInitializer, because the optional crash-reporter callback has to be captured (:78). Its doc carries the operational contract: call it once, a second call is ignored rather than doubling every report, and ordering does not matter because the handlers are installed when the app is built (HostingDependencyInjection.cs:61-67).UseCommonBarcodeScanner(Func<string> cancelText, Func<string> cameraDescription)(HostingDependencyInjection.cs:104-115) null-guards both delegates (:108-109), callsbuilder.UseBarcodeReader()to register the ZXing.Net.MAUI handlers (:111), and registers IBarcodeScannerService as a singleton built from a factory that hands both delegates to MauiBarcodeScannerService (:112-113), which invokes them once per scan.UseMauiCulture()(HostingDependencyInjection.cs:128-133) does two things:AddScoped<ICultureApplier, MauiCultureApplier>()replaces the web applier that round-trips a server/culture/setendpoint no hybrid head hosts (:130), andAddSingleton<IMauiInitializeService, MauiCultureInitializer>()restores the persisted culture at startup (:131). Calling it twice is harmless (:123-124). - Why it's built this way: the class doc pins the ordering constraint, call this after
AddUISharedinMauiProgram.CreateMauiApp(HostingDependencyInjection.cs:12-13), because DependencyInjection uses plainAddto override the shared TryAdd defaults. The culture fold-in is explained in an inline comment as a deliberate cross-ADR decision (HostingDependencyInjection.cs:36-40): culture belongs to ADR-027 rather than ADR-042, but a hybrid head that skips it ends up with a culture switcher that navigates to a server endpoint it does not host and renders the not-found page, so wiring it here means no head can be left half-configured, whileUseMauiCulture()stays public for a head that composes by hand. Barcode scanning is deliberately not folded in (HostingDependencyInjection.cs:85-87): a head that never scans should ship neither the camera handler nor a camera permission declaration, and the head still declares the platform permission itself, AndroidCAMERAand iOSNSCameraUsageDescription(HostingDependencyInjection.cs:96-100). One step the wrapper cannot take at all is the MauiCommunityToolkit registration: speech-to-text (MauiSpeechToTextService) depends on it, and the toolkit's MCT001 analyzer requires.UseMauiCommunityToolkit()to appear in the app's ownUseMauiApp<T>()chain, so the doc states the requirement rather than hiding it (HostingDependencyInjection.cs:23-28). - Where it's used: called per MAUI head in
MauiProgram.CreateMauiApp. ADC callsUseMauiDeviceCapabilities()right afterAddUIShared(MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:101,106),UseMmcaMauiErrorHandling()with no callback (:111), and the barcode scanner with resource lookups, placed after the UI module that owns the check-in surface so the plainAddis the last registration to run (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:153-155). Store callsUseMauiDeviceCapabilities()(MMCA.Store/Source/Hosts/UI/MMCA.Store.UI/MauiProgram.cs:66) andUseMmcaMauiErrorHandling()(:74) but registers no scanner. Both heads chain.UseMauiCommunityToolkit()themselves (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:60,MMCA.Store/Source/Hosts/UI/MMCA.Store.UI/MauiProgram.cs:45). - Caveats / not-in-source: the head keeps obligations this wrapper cannot fulfill: chaining
.UseMauiCommunityToolkit(), declaring the camera permission when it scans, and supplying the per-capability configuration each inert service needs (push credentials,OAuth:MobileRedirectScheme). The error handler covers managed exceptions only; a native crash, a stack overflow or a fail-fast tears the process down below the runtime and no handler here runs (HostingDependencyInjection.cs:53-60). Not determinable from source in this unit: the scan page ZXing builds at runtime, covered with MauiBarcodeScannerService elsewhere in this group.
IBatteryStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common.UI/Services/Capabilities/DeviceStatus/IBatteryStatusService.cs:8· Level 0 · interface
- What it is: exposes the platform energy-saver state (plus a change event) so live features can throttle themselves on a draining battery.
- Depends on: BCL only (
System.EventHandler). - Concept, the property plus change-event capability shape. This is the first of several
read-a-state, react-to-changes contracts (compare
IConnectivityStatusService): a bool property plus anEventHandlerthat fires after it changes, with handlers re-reading the property.[Rubric §12, Performance & Scalability]and[Rubric §23, Front-End Performance]assess whether the client adapts work to device constraints; here a component can drop a SignalR channel auto-join when the OS reports low-power mode (IBatteryStatusService.cs:4-5). Web and null fallbacks always reportfalseand never raise the event (IBatteryStatusService.cs:5-6), so a non-native head simply behaves as "never energy-saving". - Walkthrough
EnergySaverChanged(IBatteryStatusService.cs:11): raised afterIsEnergySaverOnchanges; handlers read the new value from the property rather than from event args (IBatteryStatusService.cs:10).IsEnergySaverOn(IBatteryStatusService.cs:14): whether OS energy saver or low-power mode is active right now.
- Why it's built this way: the property-plus-event shape lets a component both read the current state on render and subscribe for later transitions without polling; the always-false fallback keeps the throttling logic branch-free on non-native heads.
- Where it's used: implemented by
MauiBatteryStatusServiceand the fallbackNullBatteryStatusService, which is the TryAdd default (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:52); consumed by live and real-time components deciding whether to auto-join channels.
IBiometricAuthenticator
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Auth·MMCA.Common.UI/Services/Capabilities/Auth/IBiometricAuthenticator.cs:9· Level 0 · interface
- What it is: prompts for platform biometric or device-credential authentication (fingerprint, Face ID, Windows Hello) to gate stored-token auto-login behind an opt-in app lock.
- Depends on: BCL only.
- Concept introduced, fail-closed boolean auth gating.
[Rubric §11, Security]and[Rubric §26, Front-End Security]assess whether client-side auth degrades safely. The contract is deliberately all-booleans (IBiometricAuthenticator.cs:5-7): availability and outcome are bothboolso that on any failure the caller falls back to the normal credential login, never to a weaker path. The app-lock gated by this service is toggled throughDevicePreferenceKeys.AppLockEnabled(ADR-042 Wave 4). - Walkthrough
IsAvailableAsync(CancellationToken = default)(IBiometricAuthenticator.cs:12): whether a biometric or device-credential prompt can be presented right now.AuthenticateAsync(string reason, CancellationToken = default)(IBiometricAuthenticator.cs:19): shows the platform prompt with a localizedreason(IBiometricAuthenticator.cs:15); returnstrueonly on positive verification, and cancellation, lockout, and errors all collapse tofalse(IBiometricAuthenticator.cs:16-17).
- Why it's built this way: folding cancellation, lockout, and error into a single
falsekeeps the call site's decision binary (verified or not) and forbids a partial-success path; the localizedreasonis required because the platform surfaces it in the system prompt. - Where it's used: implemented by
MauiBiometricAuthenticatorand the inertNullBiometricAuthenticator, the TryAdd default (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:53); consumed by the auto-login app-lock gate, and faked in component tests byFakeBiometricAuthenticator. - Caveats / not-in-source: the actual token store and auto-login flow live in the head apps and the Identity layer; this contract only decides "is the user present".
IConnectivityStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common.UI/Services/Capabilities/DeviceStatus/IConnectivityStatusService.cs:10· Level 0 · interface
- What it is: reports whether the device currently has network access (with a change event and an explicit initialize step), so shared components can show an offline banner and skip doomed API calls.
- Depends on: BCL only (
System.EventHandler,System.Threading.Tasks.ValueTask). - Concept, offline-awareness at the UI edge.
[Rubric §29, Resilience & Business Continuity]assesses graceful degradation; this contract lets the UI stay usable offline rather than hang on dead requests. The doc comment records the three host behaviors (IConnectivityStatusService.cs:4-8): MAUI wrapsConnectivity.Current, WebAssembly watchesnavigator.onLine, and Blazor Server is always online (a dead circuit takes the whole UI down and the reconnect overlay already covers it). It extends the property-plus-event shape ofIBatteryStatusServicewith anInitializeAsyncbecause the browser adapter needs explicit JS listener setup. - Walkthrough
ConnectivityChanged(IConnectivityStatusService.cs:13): raised afterIsOnlinechanges; handlers read the new value from the property (IConnectivityStatusService.cs:12).IsOnline(IConnectivityStatusService.cs:16): defaults totrueuntil known (IConnectivityStatusService.cs:15), so the UI starts optimistic rather than flashing an offline banner on first render.InitializeAsync(CancellationToken = default)(IConnectivityStatusService.cs:22): starts change monitoring where that needs explicit setup (browser JS listeners); called fromOnAfterRenderAsync, a no-op and safe to call repeatedly on every implementation (IConnectivityStatusService.cs:19-20).
- Why it's built this way:
ValueTask InitializeAsynckeeps the always-ready implementations allocation-free while giving the browser adapter a place to attach listeners after the first render (JS interop is unavailable during prerender). - Where it's used: implemented by
MauiConnectivityStatusService,BrowserConnectivityStatusService(the scoped browser override,MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:100), and the framework defaultAlwaysOnlineConnectivityStatusService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:40); consumed by the offline banner and request-skipping guards, and faked in component tests byFakeConnectivityService.
IExternalAuthBroker
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Auth·MMCA.Common.UI/Services/Capabilities/Auth/IExternalAuthBroker.cs:10· Level 0 · interface
- What it is: runs an external OAuth sign-in (Google/GitHub) through the platform's system-browser authenticator instead of a web redirect, because the identity providers reject embedded WebViews.
- Depends on: BCL only.
- Concept, native OAuth callback capture.
[Rubric §11, Security]and[Rubric §26, Front-End Security]. The default broker is unavailable, which preserves the existing anchor-href redirect flow on web heads; the MAUI implementation drivesWebAuthenticatoragainst the API's OAuth endpoints and stores the resulting token pair (IExternalAuthBroker.cs:4-8). This is the client half of the native deep-link OAuth callback design (ADR-043), where the server redirects a single-use completion code to an allow-listed custom scheme soWebAuthenticatorcan capture it (never tokens over the wire). - Walkthrough
IsAvailable(IExternalAuthBroker.cs:13): whether a native brokered sign-in exists on this host (false on web heads).SignInAsync(string provider, CancellationToken = default)(IExternalAuthBroker.cs:20): runs the full brokered flow for a provider (google,github): system-browser challenge, callback capture, code exchange, token storage; returns whether the user ended up authenticated (IExternalAuthBroker.cs:16-18).
- Why it's built this way: an unavailable default means a component can attempt native brokering
and cleanly fall back to the web anchor flow when
IsAvailableis false, so one login page serves every head (ADR-043). - Where it's used: implemented by
MauiExternalAuthBroker(native) and the fallbackUnavailableExternalAuthBroker, which is the TryAdd default (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:55); consumed by the login page's external-provider buttons.
IHapticFeedbackService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common.UI/Services/Capabilities/DeviceStatus/IHapticFeedbackService.cs:8· Level 0 · interface
- What it is: fires tactile feedback on interactions (bookmark toggles, poll votes). Native-only: the web fallback is a hidden no-op.
- Depends on: BCL only (
System.TimeSpan). - Concept, decoration-not-behavior capability.
[Rubric §18, UI Architecture]. The methods are fire-and-forgetvoid(notTask) and failures are swallowed because "haptics are decoration, never behavior" (IHapticFeedbackService.cs:6), so a missing or throwing vibrator can never affect what the app does.IsSupportedisfalseon the web fallback (IHapticFeedbackService.cs:4-5). - Walkthrough
IsSupported(IHapticFeedbackService.cs:11): whether the platform can produce haptics.Click()(IHapticFeedbackService.cs:14): short feedback for taps and toggles.LongPress()(IHapticFeedbackService.cs:17): stronger feedback for long-press interactions.Vibrate(TimeSpan duration)(IHapticFeedbackService.cs:20): raw vibration for attention-level cues (for example a notification arriving while the app is foregrounded).
- Why it's built this way: synchronous
voidmatches the fire-and-forget nature of a UI micro-cue (no caller waits on a buzz), and the swallow-failures rule keeps a decorative effect out of the correctness path. - Where it's used: implemented by
MauiHapticFeedbackServiceand the no-opNullHapticFeedbackService, the TryAdd default (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:43); consumed by interactive components.
AlwaysOnlineConnectivityStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStatus/AlwaysOnlineConnectivityStatusService.cs:7· Level 1 · class (sealed)
- What it is: the default
IConnectivityStatusServicefor any head with no better answer: it reports the device permanently online and never raises a change event. The class summary states the reasoning directly, that this is the correct behavior for Blazor Server, where a lost connection tears down the circuit itself (AlwaysOnlineConnectivityStatusService.cs:3-6). - Depends on:
IConnectivityStatusServiceonly. Externals: BCLEventHandlerandValueTask. - Concept introduced, the neutral capability default. This is the first of the sixteen fallback implementations in this unit, so the shared shape is worth teaching once. Every device capability in this group is a narrow interface that shared Blazor components inject directly, and a component cannot ask "am I on MAUI or in a browser" without becoming host-aware. So the framework guarantees that every contract always resolves:
AddDeviceCapabilityDefaults()TryAdd-registers one of these fallbacks for each interface (MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:37, block:33-71), and a head that can do better calls a plainAddafterwards, so last-registration-wins swaps in the real implementation with no component change (DependencyInjection.cs:16-21). Note the vocabulary distinction this class draws: most siblings are Null Objects (they report the capability absent), but this one is a neutral default that asserts a genuinely true value on its target host, which is why it is namedAlwaysOnline...rather thanNull....- Also worth reading closely: the
ConnectivityChangedevent is declared with explicit emptyadd/removeaccessors (AlwaysOnlineConnectivityStatusService.cs:10-21) rather than as a field-like event. Subscribing therefore compiles and costs nothing, but the instance never stores a delegate, so this process-lifetime singleton can never root a subscriber component. A field-like event would have kept every disposed component alive until it unsubscribed. [Rubric §2, Design Patterns]assesses whether classic patterns are used where they earn their keep. Null Object plus Adapter is the pairing that runs through this whole capability family: one inert default, one platform adapter per head.[Rubric §1, SOLID]assesses SOLID adherence. Liskov substitutability is the entire mechanism here, since a component holds only the interface and cannot tell which implementation it received.[Rubric §22, Responsive / Cross-Browser]assesses graceful behavior across heads and browsers. One shared component tree runs unchanged on Blazor Server, WebAssembly and MAUI precisely because the container, not the component, answers the "what can this device do" question.
- Also worth reading closely: the
- Walkthrough
ConnectivityChanged(AlwaysOnlineConnectivityStatusService.cs:10-21): accessor-only event, both bodies documented no-ops ("Never raised: connectivity is constant on this host").IsOnline(AlwaysOnlineConnectivityStatusService.cs:24): a constanttrue.InitializeAsync(CancellationToken)(AlwaysOnlineConnectivityStatusService.cs:27): returnsValueTask.CompletedTask. The contract asks callers to invoke this fromOnAfterRenderAsyncand promises it is a safe repeat call on every implementation (MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStatus/IConnectivityStatusService.cs:18-22); here there is nothing to start.
- Why it's built this way: ADR-042. On Blazor Server the render tree lives on the server, so "the device went offline" is not a state the UI can render: the circuit drops and the framework reconnect overlay takes over. Reporting a fabricated offline state would double up on that overlay, so the honest answer for this host is a constant
true. - Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:49); overridden byBrowserConnectivityStatusServiceinAddBrowserDeviceCapabilities()(DependencyInjection.cs:109) and byMauiConnectivityStatusServiceon native heads (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:46). The visible consumer is the sharedOfflineBannercomponent, which renders nothing whileIsOnlineis true (MMCA.Common/Source/Presentation/MMCA.Common.UI/Components/Capabilities/OfflineBanner.razor:12-17,:27-28). MMCA.Common's bUnit base registers it explicitly so component tests get a deterministic online state (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/BunitTestBase.cs:37), ADC's Conference bUnit base gets it throughAddDeviceCapabilityDefaults()instead of a hand-mirrored list (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/BunitTestBase.cs:26), andCapabilityFallbackTestsassertsIsOnlinestays true acrossInitializeAsync(MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/Capabilities/CapabilityFallbackTests.cs:69-77).
NullBatteryStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStatus/NullBatteryStatusService.cs:4· Level 1 · class (sealed)
- What it is: the default
IBatteryStatusService: energy saver is never reported active (NullBatteryStatusService.cs:3). - Depends on:
IBatteryStatusService; BCLEventHandler. - Concept: the property-plus-change-event capability shape, reduced to its inert form. It uses the same accessor-only event trick introduced under
AlwaysOnlineConnectivityStatusService: subscription compiles, no delegate is retained, nothing is ever raised, and therefore this singleton cannot leak subscribers.[Rubric §12, Performance & Scalability]assesses whether work adapts to constraints. Answeringfalsemeans "do not throttle", which is the right default on a desktop browser or a server-rendered circuit where there is no battery to conserve.
- Walkthrough
EnergySaverChanged(NullBatteryStatusService.cs:7-18): explicit emptyadd/remove, documented as never raised because there is no battery state on this host.IsEnergySaverOn(NullBatteryStatusService.cs:21): constantfalse.
- Why it's built this way: ADR-042. A live feature asks this before deciding to poll or auto-join a real-time channel; on a host with no power constraint the honest answer is "not conserving", so the feature runs at full fidelity.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:61);MauiBatteryStatusServiceis the native override (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:47), andAddBrowserDeviceCapabilities()registers no battery implementation (DependencyInjection.cs:100-115), so web heads keep this default. Covered inCapabilityFallbackTests(CapabilityFallbackTests.cs:154).
NullBiometricAuthenticator
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Auth·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Auth/NullBiometricAuthenticator.cs:4· Level 1 · class (sealed)
- What it is: the default
IBiometricAuthenticator: biometrics unavailable, so hosts hide the app-lock toggle (NullBiometricAuthenticator.cs:3). - Depends on:
IBiometricAuthenticator; BCLTask. - Concept: fail closed. Most defaults in this unit degrade toward "nothing happens", which is safe because nothing was being protected. This one degrades toward "authentication did not succeed", which is the only safe direction for a security gate:
AuthenticateAsyncreturnsfalse, nevertrue, so a caller that skips the availability probe and treats the result as a grant still cannot unlock anything.[Rubric §11, Security]assesses whether security decisions default to denial. Both members returnfalse, so an absent capability can never be mistaken for a passed check.[Rubric §1, SOLID]assesses substitutability. The distinction matters here: a Null Object must preserve the semantics of the contract, and for an authenticator that means denial, not a convenient success.
- Walkthrough
IsAvailableAsync(CancellationToken)(NullBiometricAuthenticator.cs:7-8):Task.FromResult(false); the UI hides the app-lock setting.AuthenticateAsync(string reason, CancellationToken)(NullBiometricAuthenticator.cs:11-12):Task.FromResult(false), ignoring the reason string that a real prompt would display.
- Why it's built this way: ADR-042. App lock is a native-only affordance, and web heads already sit behind the normal auth pipeline, so the framework does not simulate a biometric prompt in a browser.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:62);MauiBiometricAuthenticatoris the only override (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:61). Both members are asserted false inCapabilityFallbackTests(CapabilityFallbackTests.cs:149-150).
NullHapticFeedbackService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStatus/NullHapticFeedbackService.cs:4· Level 1 · class (sealed)
- What it is: the default
IHapticFeedbackService: no haptics hardware, so every call is a no-op (NullHapticFeedbackService.cs:3). - Depends on:
IHapticFeedbackService; BCLTimeSpan. - Concept: the one synchronous capability contract in this unit. Its members return
void, notTask, because a haptic tick is a fire-and-forget hardware pulse that no caller ever waits on. That shape lets a component callClick()inline in an event handler without anawait, and this default makes doing so free everywhere.[Rubric §18, UI Architecture]assesses separation of presentation concerns. Feedback intent ("this was a click", "this was a long press") lives in the component; how, or whether, the device expresses it lives behind the interface.
- Walkthrough
IsSupported(NullHapticFeedbackService.cs:7): constantfalse, so a settings page can hide a haptics toggle.Click()(NullHapticFeedbackService.cs:10-13),LongPress()(:16-19) andVibrate(TimeSpan duration)(:22-25): three empty bodies, each carrying the explanatory comment "No haptics on this host" so the emptiness reads as deliberate rather than unfinished.
- Why it's built this way: ADR-042. Haptics are pure enhancement, never the carrier of information, so silently doing nothing is a complete implementation of the contract on a host without a vibrator.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:52);MauiHapticFeedbackServiceis the native override (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:50) andAddBrowserDeviceCapabilities()registers no haptics implementation (DependencyInjection.cs:100-115), so web heads keep this. ADC registers it explicitly in a bUnit test so the live-channel page renders without hardware (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/LiveChannelJoinTests.cs:59-60), andCapabilityFallbackTestsasserts all three calls are silent and non-throwing (CapabilityFallbackTests.cs:36-49).
UnavailableExternalAuthBroker
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Auth·MMCA.Common.UI/Services/Capabilities/Auth/UnavailableExternalAuthBroker.cs:7· Level 1 · class (sealed)
- What it is - the default
IExternalAuthBroker: there is no native sign-in broker on this head. It reports itself unavailable and refuses to run a brokered flow, which keeps the shared Login page on its ordinary anchor-href OAuth redirect (UnavailableExternalAuthBroker.cs:3-6). - Depends on - implements
IExternalAuthBroker; BCLTaskonly. NoNavigationManager, no configuration, no HTTP, no state. - Concept introduced - the fallback that is the correct behavior, not a degraded one. The other defaults in this unit stand in for a capability that simply is not there (no geocoder, no camera). This one is different, and the class doc is explicit about it (
UnavailableExternalAuthBroker.cs:4-5): for a web head, "no native broker" is not a shortfall, it is the right answer, because the browser already has a perfectly good OAuth flow, the redirect. Identity providers reject embedded WebViews, which is the whole reason a native broker exists at all (MMCA.Common.UI/Services/Capabilities/Auth/IExternalAuthBroker.cs:4-8); a browser has no WebView problem to solve. SoIsAvailable == falsehere means "use the flow you already have," and the login page's provider buttons take the anchor path unchanged.- Note the naming signal too. It is
Unavailable..., notNull..., the only capability default in the group named that way, matching this contract'sIsAvailableprobe (IExternalAuthBroker.cs:12-13) rather than theIsSupportedprobe the other contracts use. - [Rubric §11 - Security] §11 assesses authentication design. Making the absence of native brokering the default means no build accidentally routes an OAuth flow through an unconfigured broker; a head opts in to brokering by registering a real one.
- [Rubric §26 - Front-End Security] §26 assesses browser-side security posture. The default keeps every web head on the provider-sanctioned redirect flow rather than any in-app substitute.
- [Rubric §2 - Design Patterns] §2 assesses pattern fit; structurally this is the same Null Object shape taught at
NullGeocodingServicebelow, here applied to a two-member contract.
- Note the naming signal too. It is
- Walkthrough -
sealed classimplementing the interface (UnavailableExternalAuthBroker.cs:7).IsAvailable => false(UnavailableExternalAuthBroker.cs:10) is the probe the login page reads to decide whether to draw a brokered button at all.SignInAsync(string provider, CancellationToken cancellationToken = default)(UnavailableExternalAuthBroker.cs:13-14) ignores both arguments and returnsTask.FromResult(false), an already-completed task, so a caller that skips the probe and calls anyway gets a clean "not authenticated" answer instead of an exception.falsefromSignInAsyncmeans exactly what the contract says it means: the user did not end up authenticated (MMCA.Common.UI/Services/Capabilities/Auth/IExternalAuthBroker.cs:15-20). - Why it's built this way - ADR-043 (mobile deep links and native OAuth callback) governs the native path, and ADR-042 governs the per-head selection. One Login page has to serve three heads; giving it a resolved broker with an availability probe means it never needs to know which head it is on, and this default makes the web behavior the zero-configuration one.
- Where it's used -
TryAdd-registered as a singleton byAddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:55), and resolved by the shared Login page, which injects the broker (MMCA.Common.UI/Pages/Auth/Login.razor:17), gates each provider button onExternalAuthBroker.IsAvailable(Login.razor:88,109,130), and callsSignInAsync(provider)fromSignInWithBrokerAsync(Login.razor:182,188). With this default resolved, all three buttons stay hidden and the anchor-href path is the only one rendered. The MAUI head overrides it withMauiExternalAuthBroker, registeredAddScoped(MMCA.Common.UI.Maui/DependencyInjection.cs:76); web and Server heads keep this default, sinceAddBrowserDeviceCapabilitiescontains no broker override (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:91-106). The framework's own bUnit base registers this exact pair by hand so shared-page tests exercise the production web default (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/BunitTestBase.cs:32-36).
BrowserConnectivityStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStatus·MMCA.Common.UI/Services/Capabilities/DeviceStatus/BrowserConnectivityStatusService.cs:11· Level 2 · class (sealed,IAsyncDisposable)
- What it is: the web adapter for
IConnectivityStatusService, reportingnavigator.onLineand raising the contract's change event from the windowonline/offlinelisteners. It is the most involved browser adapter in this group because the browser pushes events back into .NET rather than being polled. - Depends on:
IConnectivityStatusService,CapabilitiesJsModule(MMCA.Common.UI/Services/Capabilities/DeviceStatus/BrowserConnectivityStatusService.cs:13,:18), andDotNetObjectReference<T>plus[JSInvokable](Microsoft.JSInterop,:1); thewatchOnline/unwatchOnlineexports (MMCA.Common.UI/wwwroot/capabilities-interop.js:92,:112). - Concept introduced, JS-to-.NET callbacks and their lifetime. Everything else in this group calls one way, from .NET into the browser. Here the browser must notify .NET when the network state flips, which means handing JS a
DotNetObjectReferencewrapping this instance, exposing a[JSInvokable]method for it to call, and disposing that reference when the scope ends or it leaks the object for the life of the circuit. It also introduces the group's deferred initialization shape: the adapter cannot subscribe during prerender, so it starts optimistic and subscribes later.[Rubric §29, Resilience & Business Continuity]assesses graceful degradation under a partial outage; the offline banner and request-skipping guards read from here.[Rubric §19, State Management]assesses ownership of client state;IsOnlinehas a private setter and one mutation path, so no consumer can desynchronize it. - Walkthrough
- Fields (
MMCA.Common.UI/Services/Capabilities/DeviceStatus/BrowserConnectivityStatusService.cs:13-15): the shared module, a nullable_selfReference(theDotNetObjectReferencehanded to JS), and a_watchingflag. ConnectivityChanged(:21) andIsOnline { get; private set; } = true(:24): the contract members. Thetrueinitializer is the deliberate optimistic default the class doc calls out (:7-9): beforeInitializeAsyncruns there is no way to ask the browser, and assuming offline would flash a false banner on every first render.InitializeAsync(CancellationToken = default)(:27-47): returns immediately when already_watching(:29-32), lazily creates_selfReferencewith??=(:34), and invokeswatchOnlinepassing that reference (:35-37). The return value is the tri-state:nullmeans JS was unavailable (prerender), so the method returns without setting_watching, leaving the subscription to be retried on a later call (:39-43). Otherwise it latches_watching = trueand applies the reported state (:45-46).OnBrowserConnectivityChanged(bool isOnline)(:50-51): the[JSInvokable]callback target, documented as not for app code (:49). On the JS side both theonlineandofflinewindow listeners call the samenotifyclosure, which readsnavigator.onLinefresh and swallows a rejected invoke when the component is gone (MMCA.Common.UI/wwwroot/capabilities-interop.js:95-103).DisposeAsync()(:54-63): callsunwatchOnlineonly when it actually subscribed, withCancellationToken.None(teardown must not be cancelled,:58), then disposes and nulls_selfReference(:61-62).UpdateStatus(bool isOnline)(:65-74): the single mutation path. It returns early when the value is unchanged (:67-70), so the event fires only on a real transition, then setsIsOnlineand raisesConnectivityChanged(:72-73).
- Fields (
- Why it's built this way: the class doc instructs callers to invoke
InitializeAsyncfromOnAfterRenderAsync(MMCA.Common.UI/Services/Capabilities/DeviceStatus/BrowserConnectivityStatusService.cs:8-9), which is the first lifecycle point where JS is guaranteed available; thenull-means-retry path is what makes a call from a too-early lifecycle method harmless rather than permanently broken.watchOnlineitself callsunwatchOnlinefirst (MMCA.Common.UI/wwwroot/capabilities-interop.js:93), so a double subscribe cannot stack listeners. The change-only event contract keeps the offline banner from re-rendering on every duplicate browser event. - Where it's used: registered scoped as
IConnectivityStatusServicebyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:100); siblings areMauiConnectivityStatusServiceand the Server defaultAlwaysOnlineConnectivityStatusService. - Caveats / not-in-source:
navigator.onLinereports link-layer connectivity, not gateway reachability, so a captive-portal network reads as online here. Note also that the JScatcharm returnstrue(MMCA.Common.UI/wwwroot/capabilities-interop.js:105-109), so a browser that refusesaddEventListenerstill latches_watchingand reports online without ever pushing a change.
DevicePreferenceKeys
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common.UI/Services/Capabilities/DeviceStorage/DevicePreferenceKeys.cs:7· Level 0 · class (static)
- What it is: a static constants holder for the string keys used with
IDevicePreferences, so the framework's device-settings surfaces and the gates that read them agree on one spelling. - Depends on: nothing first-party at the type level; the doc comment names
IDevicePreferencesas the store these keys are used against (DevicePreferenceKeys.cs:5). - Concept introduced, per-device (non-roaming) preference keys.
[Rubric §19, State Management]assesses whether client state has a clear owner and scope; these keys are explicitly device state (they "describe THIS device and never roam",DevicePreferenceKeys.cs:5), distinct from the server-side per-user preferences that follow a signed-in account across devices. Centralizing the key strings is the small[Rubric §15, Best Practices & Code Quality]discipline that keeps a writer and a reader from drifting apart on a literal. - Walkthrough: one member today:
AppLockEnabled = "applock.enabled"(DevicePreferenceKeys.cs:10), whether the biometric app-lock guards stored-token auto-login. The doc comment ties it to the biometric app-lock feature (ADR-042 Wave 4,DevicePreferenceKeys.cs:9). - Why it's built this way: a
public const stringis compile-time inlined and usable in attribute and switch positions; a single owner for the key means the gate that reads it (IBiometricAuthenticatorconsumers) and the settings UI that writes it cannot disagree. - Where it's used: read and written through
IDevicePreferencesby the app-lock gate and device-settings screens in the head apps (ADR-042).
GeoPoint
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/GeoPoint.cs:9· Level 0 · record (sealed)
- What it is: a transport-agnostic latitude/longitude pair returned by
IGeolocationService, with a helper to measure great-circle distance to another point. - Depends on: BCL only (
System.Math,System.ArgumentNullException). - Concept introduced, a platform-free geo primitive.
[Rubric §18, UI Architecture]assesses whether shared UI code stays decoupled from platform types;GeoPointexists so shared components "never touch platform location types" (GeoPoint.cs:5): the MAUILocationor browser Geolocation result is mapped into this record at the adapter boundary. Being arecordgives it value equality and immutability for free (the Value Object idea, see primer §2), though it lives in the UI layer rather than the domain. - Walkthrough
- Positional parameters
Latitude/Longitudein decimal degrees (GeoPoint.cs:9, documented atGeoPoint.cs:7-8). EarthRadiusKm = 6371.0(GeoPoint.cs:11): the mean Earth radius constant the distance formula uses.DistanceKmTo(GeoPoint other)(GeoPoint.cs:17): null-guardsotherwithArgumentNullException.ThrowIfNull(GeoPoint.cs:19), then computes the haversine great-circle distance in kilometers (GeoPoint.cs:21-28). The doc comment scopes it honestly: good enough for "how far is the venue" hints, not for navigation (GeoPoint.cs:15).ToRadians(double degrees)(GeoPoint.cs:31): a private static degree-to-radian conversion, an expression-bodied member.
- Positional parameters
- Why it's built this way: keeping the math on the value type (rather than in a service) means any caller holding two points can compute a distance without a service dependency; the sealed record keeps it cheap and comparable.
- Where it's used: produced by
IGeolocationServiceimplementations (MauiGeolocationService,NullGeolocationService) and consumed by proximity hints in the head apps.
IDevicePreferences
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common.UI/Services/Capabilities/DeviceStorage/IDevicePreferences.cs:11· Level 0 · interface
- What it is: a small typed key/value store for per-device settings (reminder lead time, haptics toggle, app-lock), distinct from the server-side per-user preferences that roam with an account.
- Depends on: BCL only; keys come from
DevicePreferenceKeys. - Concept introduced, device-scoped client state.
[Rubric §19, State Management]assesses whether state has a clear owner and lifetime. Device preferences "describe THIS device and never roam" (IDevicePreferences.cs:5-6), the counterpart to the server-sideIUserPreferenceWriter(culture, theme). The doc comment sets two hard rules: never store secrets here, tokens belong in platform secure storage (IDevicePreferences.cs:7), and the supported value types are exactlystring,bool,int,long,double,DateTimeOffset(IDevicePreferences.cs:8-9).[Rubric §26, Front-End Security]: the secrets prohibition keeps sensitive material off unencrypted preference storage. - Walkthrough
IsPersistent(IDevicePreferences.cs:17): whether values survive an app restart; the Blazor Server fallback is in-memory only (false) and hosts hide device-settings UI when it is not persistent (IDevicePreferences.cs:14-15).GetAsync<T>(string key, T fallback, CancellationToken = default)(IDevicePreferences.cs:21): reads a value, returningfallbackwhen absent or unreadable (IDevicePreferences.cs:19).SetAsync<T>(string key, T value, CancellationToken = default)(IDevicePreferences.cs:25): best-effort write, storage failures are swallowed (IDevicePreferences.cs:23).RemoveAsync(string key, CancellationToken = default)(IDevicePreferences.cs:28): removes a value; unknown keys are ignored (IDevicePreferences.cs:27).
- Why it's built this way: the
IsPersistentflag lets a host decide whether to even show device-settings UI (pointless when settings would evaporate on reload), and the swallow-on-failure writes keep a cosmetic preference from ever throwing into a render path. - Where it's used: implemented by
MauiDevicePreferences,BrowserDevicePreferences(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:101), and theInMemoryDevicePreferencesfallback. This is the one capability whose default is registeredTryAddScopedrather thanTryAddSingleton(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:74), because the in-memory Blazor Server fallback must hold per-circuit (per-user) state and never cross-user state (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:72-73). Read and written by device-settings screens and the app-lock gate, and faked in component tests byFakeDevicePreferences.
ILocalCacheStore
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common.UI/Services/Capabilities/DeviceStorage/ILocalCacheStore.cs:9· Level 0 · interface
- What it is: a small on-device JSON document cache for offline UI state (an offline schedule snapshot), explicitly not a query cache and not for secrets.
- Depends on: BCL only (generic serialization happens in the implementations).
- Concept, last-known-good UI state for offline rendering.
[Rubric §29, Resilience & Business Continuity]and[Rubric §19, State Management]. MAUI persists to the app data directory, WebAssembly tolocalStorage, and the Blazor Server fallback is unavailable (SSR always has the live API) (ILocalCacheStore.cs:4-6). The doc comment draws the boundary sharply: this is last-known-good UI state for offline rendering, not a general query cache and not a secret store (ILocalCacheStore.cs:6-7). - Walkthrough
IsAvailable(ILocalCacheStore.cs:12): whether cached values survive restarts on this host.SetAsync<T>(string key, T value, CancellationToken = default)(ILocalCacheStore.cs:16): serializes and stores a JSON-serializable document, best-effort (ILocalCacheStore.cs:14-15).GetAsync<T>(string key, CancellationToken = default)(ILocalCacheStore.cs:20): reads and deserializes, or returnsdefault(ILocalCacheStore.cs:18).RemoveAsync(string key, CancellationToken = default)(ILocalCacheStore.cs:23): removes an entry; unknown keys are ignored (ILocalCacheStore.cs:22).
- Why it's built this way: a generic serialize/deserialize contract keeps callers from touching
platform storage APIs, and
IsAvailablelets a component skip offline-snapshot writes entirely on the Server head where they would be pointless. - Where it's used: implemented by
MauiLocalCacheStore,BrowserLocalCacheStore(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:102), and the unavailableNullLocalCacheStoredefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:56); consumed by offline-schedule components.
IMapNavigationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/IMapNavigationService.cs:8· Level 0 · interface
- What it is: opens the platform maps experience for a street address (native maps app on MAUI, a maps website in a new tab in browsers).
- Depends on: BCL only.
- Concept, address-only navigation.
[Rubric §18, UI Architecture]. Deliberately address-based, not coordinate-based, because the domain model carries no geo-coordinates (IMapNavigationService.cs:4-6). That keeps the capability aligned with what the data actually holds. - Walkthrough:
OpenAddressAsync(string address, string? label, CancellationToken = default)(IMapNavigationService.cs:14): opens maps pointed ataddress, labeledlabelwhere the platform supports it; returns whether a maps UI was opened (IMapNavigationService.cs:11-12). - Why it's built this way: returning a
boollets a "Directions" affordance stay hidden or degrade when no maps UI opened; taking a string address (not aGeoPoint) matches the address-shaped domain data and avoids a geocoding round-trip. - Where it's used: implemented by
MauiMapNavigationService,BrowserMapNavigationService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:103), and the defaultNullMapNavigationService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:44); consumed by venue and location components.
IGeocodingService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/IGeocodingService.cs:9· Level 1 · interface
- What it is: resolves a street address to coordinates for proximity hints such as "~3 km from the venue" (
IGeocodingService.cs:3-8). It is the address-to-coordinate half of the location story; IGeolocationService is the device-position half. - Depends on: GeoPoint, its return shape;
System.Threading.CancellationTokenotherwise. - Concept introduced: the best-effort-by-contract capability. Unsupported hosts and failed lookups both return
nulland callers simply omit the hint (IGeocodingService.cs:5-7), so a location feature never becomes a hard dependency on a platform geocoder. The doc comment also records a real domain fact: the model deliberately carries addresses only and no coordinates, so this service is the single place coordinates ever exist.[Rubric §29, Resilience & Business Continuity]§29 assesses graceful degradation. The null-on-failure, hint-is-optional contract means a geocoder outage degrades to "no proximity hint", never to a broken page.
- Walkthrough: two members.
IsSupported(IGeocodingService.cs:12): whether the platform can geocode at all; web and null fallbacks reportfalse.GeocodeAsync(string, CancellationToken)(IGeocodingService.cs:15): returns the first coordinate match for the address, ornull.
- Why it's built this way: geocoding is a native and optional concern, so it hides behind a platform-free interface with a null default and a native override selected per host (ADR-042).
- Where it's used: registered with the NullGeocodingService default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:46), overridden on native heads by MauiGeocodingService; consumed by venue-proximity UI.
IGeolocationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/IGeolocationService.cs:8· Level 1 · interface
- What it is: a soft, one-shot device location read for the same proximity hints, the device-position sibling of IGeocodingService (
IGeolocationService.cs:3-7). - Depends on: GeoPoint, its return shape;
System.Threading.CancellationToken. - Concept introduced: nothing new, it applies the same best-effort contract. Its distinct behavioral note is the at-most-once permission prompt: it returns the last-known position when that is fresh enough, otherwise a single current-position read, triggering the platform permission prompt at most once and returning
nullon denial, timeout, or any platform failure (IGeolocationService.cs:13-18).[Rubric §26, Front-End Security]§26 assesses handling of sensitive capabilities. Location is permission-gated, prompted at most once, and never blocks a feature, so the app can neither nag nor hard-depend on a sensitive grant.
- Walkthrough: two members.
IsSupported(IGeolocationService.cs:11): whether the platform can provide a location at all.GetCurrentOrLastKnownAsync(CancellationToken)(IGeolocationService.cs:18): the fresh-last-known-or-single-read behavior described above.
- Why it's built this way: same per-host swappable design as its geocoding sibling, a platform-free contract with an inert default (ADR-042).
- Where it's used: registered with the NullGeolocationService default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:45), overridden by MauiGeolocationService; consumed alongside geocoding for proximity hints, whose distance math lives on GeoPoint.
BrowserMapNavigationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/BrowserMapNavigationService.cs:9· Level 1 · class (sealed)
- What it is: the web adapter for
IMapNavigationService. There is no maps app to launch in a browser, so it builds a Google Maps search URL for the address and hands it toIExternalLinkServiceto open in a new tab. - Depends on:
IMapNavigationService(the contract it implements,MMCA.Common.UI/Services/Capabilities/Geo/BrowserMapNavigationService.cs:9) andIExternalLinkService(constructor-injected,:15,:18-19); BCLUriandUri.EscapeDataString. Unlike every other browser adapter in this group it takes noCapabilitiesJsModule: it composes over a sibling capability instead of calling JS itself. - Concept introduced, capability composition. Most adapters in this group sit directly on one platform API; this one is built entirely out of another capability, which is why it lands at Level 1 while its JS-backed siblings sit at Level 2. The payoff is that the "open something outside the app" policy (a new tab on the web, the system browser on native) is decided once in
IExternalLinkServiceand every consumer inherits it.[Rubric §2, Design Patterns]assesses whether patterns are applied with intent rather than by habit; adapter-over-adapter here avoids a second copy of the window-open rules.[Rubric §22, Responsive/Cross-Browser]assesses whether the app spans device classes gracefully; the sameOpenAddressAsynccall site produces a native maps intent on a phone and a maps tab in a desktop browser. - Walkthrough
MapsSearchUrl(MMCA.Common.UI/Services/Capabilities/Geo/BrowserMapNavigationService.cs:14): the constanthttps://www.google.com/maps/search/?api=1&query=prefix, wrapped in a scoped#pragma warning disable S1075(:11,:13) with a comment stating that the public Maps search endpoint IS the integration point on the web, so there is nothing environment-dependent to configure (:9-10).- The constructor (
:18-19) is an expression body capturing the injectedIExternalLinkServiceinto_externalLinkService(:15). OpenAddressAsync(string address, string? label, CancellationToken = default)(:22): guards withArgumentException.ThrowIfNullOrWhiteSpace(address)(:24), appendsUri.EscapeDataString(address)to the constant to build theUri(:26), awaits_externalLinkService.OpenAsync(uri, cancellationToken)(:27), and returnstrue(:28). Thelabelparameter is accepted for contract parity and is unused here.
- Why it's built this way: the web has no address-to-map handoff of its own, so a search URL is the honest equivalent; escaping the address is what keeps a street name containing
&or#from truncating the query. Delegating the open (rather than callingwindow.opendirectly) keeps thenoopener,noreferrerhardening in one place (ADR-042). - Where it's used: registered scoped as
IMapNavigationServicebyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:103); its siblings areMauiMapNavigationServiceandNullMapNavigationService. Consumed by venue and location components. - Caveats / not-in-source: the
truereturn is unconditional. BecauseOpenAsyncreturnsTask(not a success flag) and the underlyingwindow.openresult is discarded byCapabilitiesJsModule, a popup blocked by the browser still reports success to the caller. Not determinable from source: whether any consumer branches on thatbooltoday.
InMemoryDevicePreferences
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStorage/InMemoryDevicePreferences.cs:10· Level 1 · class (sealed)
- What it is: the default
IDevicePreferences, a working key/value store that keeps its values in a dictionary and loses them when the scope ends. It is honest about that throughIsPersistent, which hosts consult to hide device-settings UI that would not survive a restart (InMemoryDevicePreferences.cs:5-9). - Depends on:
IDevicePreferences. Externals: BCLConcurrentDictionary<TKey, TValue>(InMemoryDevicePreferences.cs:1),StringComparer.Ordinal,ArgumentException.ThrowIfNullOrWhiteSpace. - Concept introduced, the fallback that carries state, and the one scoped registration. Every other default in this unit is stateless and registered as a singleton. This one holds a dictionary, and that single difference changes its lifetime:
AddDeviceCapabilityDefaults()registers it withTryAddScoped, with the reason spelled out at the call site, so the Blazor Server fallback holds per-circuit (per-user) state and never cross-user state (DependencyInjection.cs:81-83). A singleton here would be a real defect: one user's reminder lead time or haptics toggle would become everyone's. The second idea worth taking away is the difference between a capability failure and a programming error. This family never throws when the device cannot do something, but a blank preference key is a caller bug, so all three methods guard it withArgumentException.ThrowIfNullOrWhiteSpaceand do throw (InMemoryDevicePreferences.cs:20,:30,:39).[Rubric §19, State Management]assesses where client state lives and how long it survives. This class makes the volatility explicit throughIsPersistentinstead of pretending durability, so a feature can decide whether to offer a setting at all.[Rubric §11, Security]assesses handling of sensitive data. Scoping to the circuit is the isolation boundary, and the contract reinforces the rule that secrets never belong here: tokens go to platform secure storage (MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStorage/IDevicePreferences.cs:5-7).[Rubric §14, Testability]assesses how easily behavior can be exercised without infrastructure. Because it is a real, dependency-free implementation rather than a no-op, downstream tests use it directly as a preferences double.
- Walkthrough
_values(InMemoryDevicePreferences.cs:12): aConcurrentDictionary<string, object?>built withStringComparer.Ordinal. Ordinal comparison means preference keys are exact byte-for-byte matches, never culture-folded.IsPersistent(InMemoryDevicePreferences.cs:15): constantfalse.GetAsync<T>(string key, T fallback, CancellationToken)(InMemoryDevicePreferences.cs:18): guards the key (:20), then returns the stored value only when the entry exists and the boxed value pattern-matchesT(stored is T typed,:22); anything else yieldsfallback(:22-24). That type test is what implements the contract's "returns fallback when absent or unreadable" rule (IDevicePreferences.cs:19).SetAsync<T>(string key, T value, CancellationToken)(InMemoryDevicePreferences.cs:28): guards the key, assigns through the indexer (an upsert), and returnsTask.CompletedTask(:30-33).RemoveAsync(string key, CancellationToken)(InMemoryDevicePreferences.cs:37): guards the key and callsTryRemovewith a discard, so an unknown key is silently ignored as the contract requires (:39-42).- Every method is synchronous under an async signature: the interface is
Task-shaped because the persistent implementations do real I/O, and this one satisfies it with already-completed tasks rather than an offloaded call.
- Why it's built this way: ADR-042. Device preferences describe this device and deliberately never roam, which is why they are separate from the server-side per-user preferences (
IDevicePreferences.cs:4-6). A Blazor Server circuit is not a device and has no durable per-device store, so the truthful default is a volatile one plus a flag that lets the UI adapt (IDevicePreferences.cs:13-17). - Where it's used:
TryAddScopedinAddDeviceCapabilityDefaults()(DependencyInjection.cs:83); overridden byBrowserDevicePreferences(localStorage) inAddBrowserDeviceCapabilities()(DependencyInjection.cs:110) and byMauiDevicePreferenceson native heads (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:59). ADC's Engagement UI tests instantiate it as the preferences double forSessionReminderCoordinatorandSessionBookmarkUIService(MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Services/HappeningNow/SessionReminderCoordinatorTests.cs:24,.../SessionBookmarkUIServiceTests.cs:128), andCapabilityFallbackTestscovers the round trip plus the not-persistent flag (CapabilityFallbackTests.cs:95-108). - Caveats / not-in-source: values are stored boxed as
object?, so aGetAsync<int>against a key written asstringreturns the fallback rather than reporting a type mismatch (InMemoryDevicePreferences.cs:22). The interface enumerates the supported value types (IDevicePreferences.cs:8-9); nothing in this class enforces that list.
NullLocalCacheStore
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DeviceStorage/NullLocalCacheStore.cs:4· Level 1 · class (sealed)
- What it is: the default
ILocalCacheStore: nothing is cached and reads returndefault(NullLocalCacheStore.cs:3). - Depends on:
ILocalCacheStore; BCLTask. - Concept: the write-succeeds, read-misses fallback.
SetAsynccompletes normally whileGetAsyncstill answersnull, which is deliberately not a contradiction: a cache write is advisory, and every caller must already handle a miss because a real cache can evict at any moment. Treating a completed write as a promise of a later hit would be a bug against any cache implementation, not just this one.[Rubric §29, Resilience & Business Continuity]assesses degradation when a dependency is absent. Offline-first features stay compilable and runnable on a host with no local storage; they just never serve a cached document.[Rubric §12, Performance & Scalability]assesses avoidable work.IsAvailablelets a feature skip building and serializing a cache payload it knows will be discarded.
- Walkthrough
IsAvailable(NullLocalCacheStore.cs:7): constantfalse.SetAsync<T>(string key, T value, CancellationToken)(NullLocalCacheStore.cs:10-11): returnsTask.CompletedTask, discarding the value.GetAsync<T>(string key, CancellationToken)(NullLocalCacheStore.cs:14-15): returnsTask.FromResult<T?>(default), so reference types read back asnulland value types as zero.RemoveAsync(string key, CancellationToken)(NullLocalCacheStore.cs:18):Task.CompletedTask.
- Why it's built this way: ADR-042. Offline caching is a per-head capability (browser
localStorage, native file or preference storage), and a Blazor Server circuit has no client-side store it can reach without JavaScript, so the default is the empty cache. - Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:65); overridden byBrowserLocalCacheStore(DependencyInjection.cs:111) andMauiLocalCacheStore(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:60).CapabilityFallbackTestswrites then reads backnullto pin the behavior (CapabilityFallbackTests.cs:110-118).
NullMapNavigationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Geo/NullMapNavigationService.cs:4· Level 1 · class (sealed)
- What it is: the default
IMapNavigationService: no maps integration, so opening an address reports failure (NullMapNavigationService.cs:3). - Depends on:
IMapNavigationService; BCLTask. - Concept: an outcome-returning fallback in the same shape as
NullClipboardService, with noIsSupportedprobe. Afalseanswer is the component's cue to leave the address as plain text rather than a tappable "open in maps" affordance. - Walkthrough: one member.
OpenAddressAsync(string address, string? label, CancellationToken)(NullMapNavigationService.cs:7-8) returnsTask.FromResult(false), ignoring both the address and the optional pin label. - Why it's built this way: ADR-042. Launching a map is a platform handoff (a native maps app, or a maps URL in a new tab), and a head that has neither should not fabricate one.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:53); overridden byBrowserMapNavigationService(DependencyInjection.cs:112) andMauiMapNavigationService(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:51). Covered inCapabilityFallbackTests(CapabilityFallbackTests.cs:148).
BrowserDevicePreferences
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserDevicePreferences.cs:10· Level 2 · class (sealed)
- What it is: the web adapter for
IDevicePreferences, a typed key/value store for per-device settings backed bylocalStoragewith JSON-encoded values under themmca.devicePrefs.prefix. - Depends on:
IDevicePreferencesandCapabilitiesJsModule(MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserDevicePreferences.cs:14,:17); BCLSystem.Text.Json(:1); thestorageGet/storageSet/storageRemoveexports (MMCA.Common.UI/wwwroot/capabilities-interop.js:130,:138,:147). - Concept introduced, cross-head storage parity by convention. This adapter and
MauiDevicePreferencesindependently agree on the same key prefix and the same encoding (one JSON document per value), so a setting written by a component behaves identically on either head with no per-type platform code.[Rubric §19, State Management]assesses whether state has a clear owner and lifetime; these values describe one device or browser profile and never roam to the server.[Rubric §26, Front-End Security]applies by exclusion:localStorageis readable by any script on the origin, which is why tokens live inITokenStorageServiceinstead. - Walkthrough
KeyPrefix = "mmca.devicePrefs."(MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserDevicePreferences.cs:12) and the injected module (:14,:17).IsPersistent => true(:20): values survive a session on the same browser profile.GetAsync<T>(string key, T fallback, CancellationToken = default)(:23-44): guards the key (:25), reads the prefixed raw string (:27-29), returnsfallbackwhen it isnull(which covers both "absent" and "JS unavailable",:30-33), thenJsonSerializer.Deserialize<T>inside atry, returningfallbackon a null result or aJsonException(:35-43).SetAsync<T>(string key, T value, CancellationToken = default)(:47-55): guards the key (:49), serializes to JSON (:51), and invokesstorageSetwith the prefixed key (:52-54); thebool?result is discarded.RemoveAsync(string key, CancellationToken = default)(:58-65): guards (:60) and invokesstorageRemove(:62-64).
- Why it's built this way:
ArgumentException.ThrowIfNullOrWhiteSpace(key)is the one place this class does throw, because an empty key is a programming error, not an environment condition. Everything environmental (Safari Private Browsing, an iframe with storage disabled, a corrupt value) degrades to the caller'sfallback, so a preferences read can never break a render path (:5-8). Discarding the write result is consistent: there is no useful UI response to "the browser refused to persist your preference". - Where it's used: registered scoped as
IDevicePreferencesbyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:101); siblings areMauiDevicePreferencesand the in-memory Server defaultInMemoryDevicePreferences, which isTryAddScopedprecisely so a Blazor Server circuit holds per-user rather than cross-user state (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:72-74). Keys come fromDevicePreferenceKeys.
BrowserLocalCacheStore
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.DeviceStorage·MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserLocalCacheStore.cs:10· Level 2 · class (sealed)
- What it is: the web adapter for
ILocalCacheStore, holding last-known-good JSON snapshots inlocalStorageunder themmca.localCache.prefix so a page can still render something when the API is unreachable. - Depends on:
ILocalCacheStoreandCapabilitiesJsModule(MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserLocalCacheStore.cs:14,:17); BCLSystem.Text.Json(:1); the same threestorage*exportsBrowserDevicePreferencesuses. - Concept introduced: nothing new mechanically (it is the storage shape
BrowserDevicePreferencesintroduces), but the separation of the two stores under different prefixes is the point: preferences are the user's settings and should never be evicted to make room for a stale snapshot, while cache entries are disposable. The class doc records the practical ceiling: browsers caplocalStoragearound 5 MB, so callers keep documents lean (:7-8).[Rubric §29, Resilience & Business Continuity]assesses whether the client degrades usefully when a dependency is down; this store is what an offline view renders from.[Rubric §23, Front-End Performance]applies because a warm snapshot removes a blocking fetch from first paint. - Walkthrough
KeyPrefix = "mmca.localCache."(MMCA.Common.UI/Services/Capabilities/DeviceStorage/BrowserLocalCacheStore.cs:12);IsAvailable => true(:20), since a browser head always haslocalStorageto attempt (a refusal degrades per call rather than being predicted here).SetAsync<T>(...)(:23-31): guards the key (:25),JsonSerializer.Serialize(:27), invokesstorageSet(:28-30), discards the result. A failed write only means a colder next visit.GetAsync<T>(...)(:34-54): guards the key (:36), reads the raw string (:38-40), returnsdefaultwhen it isnull(:41-44), and deserializes inside atrythat mapsJsonExceptiontodefault(:46-53). TheJsonExceptionarm is what makes a schema change survivable: a document written by an older version of the app that no longer deserializes is treated as a cache miss, not an error.RemoveAsync(...)(:57-64): guards (:59) and invokesstorageRemove(:61-63).
- Why it's built this way:
defaulton every failure means a cache miss and a corrupt entry are the same event to the caller: fetch live. Storing pre-serialized strings from C# (the JS helpers deliberately treat values as opaque raw strings,MMCA.Common.UI/wwwroot/capabilities-interop.js:127-128) keeps all typing on the .NET side, so the sameTround-trips identically on every head. - Where it's used: registered scoped as
ILocalCacheStorebyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:102); siblings areMauiLocalCacheStore(file-per-key on disk) and the unavailableNullLocalCacheStore. - Caveats / not-in-source: nothing here evicts or expires entries, and
IsAvailableis a constant rather than a probe, so a quota-exhausted profile still reports available and simply fails each write.
NullGeocodingService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/NullGeocodingService.cs:4· Level 2 · class (sealed)
- What it is - the inert default for
IGeocodingService: a geocoder that geocodes nothing. It reports itself unsupported and hands backnullfor every address, which is exactly the "no coordinate hint available" state the contract is designed around (NullGeocodingService.cs:3). - Depends on - implements
IGeocodingService; returnsGeoPoint?. No other first-party or external dependency beyondSystem.Threading.Tasks(Task.FromResult). - Concept introduced - the null-object capability fallback. This is the first of five inert
Null...defaults in this unit, so the shape is worth teaching once here. Every device capability in this group is an interface (geocoding, geolocation, notifications, media picking, push, and so on), and shared Blazor components resolve those interfaces directly by injection. But a plain web head has no native geocoder, and a prerendering circuit has no JavaScript yet, so something must be in the container or resolution throws. The framework fills the container with a Null Object implementation for every contract: a real, substitutable instance whose methods do nothing observable, rather than anullreference the caller has to check for. The two moving parts are theIsSupportedflag (a component reads it and hides the affordance) and the operation itself (a component that ignoresIsSupportedand calls anyway still gets a safe answer, never an exception).AddDeviceCapabilityDefaultsTryAdd-registers this class as a singleton (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:46), and a native or richer head later overrides it with a plainAdd, so last-registration-wins swaps the real implementation in without the shared component knowing (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:16-21). Every other fallback in this chapter, including the stateless siblings such asNullShareService, follows this same shape.- [Rubric §2 - Design Patterns] §2 assesses whether classic patterns are applied where they earn their keep; this is a textbook Null Object, a do-nothing implementation that removes null-checks and host-detection branches from every caller.
- [Rubric §1 - SOLID] §1 assesses SOLID adherence; the substitutability here is Liskov and Dependency Inversion in practice, components depend on the abstraction and any implementation (null or native) satisfies it interchangeably.
- [Rubric §22 - Responsive / Cross-Browser] §22 assesses graceful behavior across heads and browsers; the null default is what lets one shared component tree run unchanged on web, where geocoding does not exist.
- Walkthrough -
sealed classimplementing the interface (NullGeocodingService.cs:4).IsSupported => false(NullGeocodingService.cs:7) tells callers to omit the proximity hint entirely.GeocodeAsync(NullGeocodingService.cs:10-11) ignores itsaddressandcancellationTokenarguments and returnsTask.FromResult<GeoPoint?>(null), an already-completed task, so there is no async state machine and no thread hop. - Why it's built this way - ADR-042 (device capability abstraction). The domain model deliberately carries addresses and no coordinates, so geocoding is a pure presentation-time convenience and this contract is "the only place they ever exist" (
MMCA.Common.UI/Services/Capabilities/Geo/IGeocodingService.cs:3-8). Making the unsupported case a first-classnull(rather than an exception or a feature flag the caller must invent) keeps proximity hints optional everywhere. - Where it's used - registered by
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:46) and resolved by any component that shows a distance-from-venue hint, for example ADC's public event detail page, which injects both location contracts and bails out of the hint when either reports unsupported (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Public/Events/PublicEventDetail.razor.cs:26-27,214). The MAUI head replaces it withMauiGeocodingService(MMCA.Common.UI.Maui/DependencyInjection.cs:53); there is no browser override, so web and Server heads keep this null default.
NullGeolocationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Geo·MMCA.Common.UI/Services/Capabilities/Geo/NullGeolocationService.cs:4· Level 2 · class (sealed)
- What it is - the inert default for
IGeolocationService: no location source. It reports unsupported and returnsnullfor the current position, so a caller simply omits any proximity hint (NullGeolocationService.cs:3). - Depends on - implements
IGeolocationService; returnsGeoPoint?. Same null-object shape asNullGeocodingService; see there for the pattern. - Concept introduced - none new. This is the sibling of
NullGeocodingServicefor the "where is this device" half of the location story (geocoding turns an address into a point, geolocation reads the device's own point). The same [Rubric §1 - SOLID], [Rubric §2 - Design Patterns], and [Rubric §22 - Responsive / Cross-Browser] notes apply. - Walkthrough -
sealed class(NullGeolocationService.cs:4).IsSupported => false(NullGeolocationService.cs:7).GetCurrentOrLastKnownAsyncreturnsTask.FromResult<GeoPoint?>(null)(NullGeolocationService.cs:10-11); because it never touches the platform it also never fires the OS permission prompt the real contract warns about (MMCA.Common.UI/Services/Capabilities/Geo/IGeolocationService.cs:13-18), which is the desired behavior on a head that could not honor a grant anyway. - Why it's built this way - ADR-042. Location is soft and best-effort by contract: permission denial, timeout, or any platform failure already yields
null(MMCA.Common.UI/Services/Capabilities/Geo/IGeolocationService.cs:3-6,14-16), so a head with no location provider is just the permanent version of that same "no fix" outcome, and no caller needs a second code path for it. - Where it's used - registered by
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:45); consumed alongside the geocoder by ADC's public event detail page (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Public/Events/PublicEventDetail.razor.cs:26,214). The MAUI head overrides it withMauiGeolocationService(MMCA.Common.UI.Maui/DependencyInjection.cs:52); web and Server heads keep this default, sinceAddBrowserDeviceCapabilitiesregisters no geolocation implementation (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:91-106).
IClipboardService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/IClipboardService.cs:7· Level 0 · interface
- What it is: writes text to the system clipboard, returning success so a caller can confirm with a snackbar.
- Depends on: BCL only.
- Concept, best-effort capability with a success return.
[Rubric §18, UI Architecture]. A single method wraps MAUIClipboard.Defaultand browsernavigator.clipboard(IClipboardService.cs:4-5); returningbool(rather thanvoid) lets the UI acknowledge only when the write actually landed, which matters because browser clipboard writes can be denied by permission. - Walkthrough:
SetTextAsync(string text, CancellationToken = default)(IClipboardService.cs:10): copiestext, returns whether the write succeeded. - Why it's built this way: the boolean result is the fallback signal for
IShareServicecallers: when a native share sheet is unavailable, they copy the link instead and confirm from this return. - Where it's used: implemented by
MauiClipboardService,BrowserClipboardService(registered per scope inAddBrowserDeviceCapabilities,MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:97), and the defaultNullClipboardService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:42); it is the copy-link fallback path ofIShareService.
IExternalLinkService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/IExternalLinkService.cs:9· Level 0 · interface
- What it is: opens URLs outside the current UI surface (a new browser tab, or the system browser from inside a BlazorWebView).
- Depends on: BCL only (
System.Uri). - Concept, the WebView dead-link workaround.
[Rubric §18, UI Architecture]and[Rubric §25, Navigation & IA]. A rawtarget="_blank"silently dead-ends inside a WKWebView, so shared components must route external links through this service (via theExternalLinkcomponent) rather than raw anchor targets (IExternalLinkService.cs:4-7). TheInterceptsLinksflag lets the component pick the cheapest correct rendering per host. - Walkthrough
InterceptsLinks(IExternalLinkService.cs:16): whether links must be intercepted and opened viaOpenAsync(truein native WebView hosts); whenfalse, components may render a plain anchor withtarget="_blank"(IExternalLinkService.cs:12-14).OpenAsync(Uri uri, CancellationToken = default)(IExternalLinkService.cs:19): opens the URI in the system browser or a new tab, best-effort (IExternalLinkService.cs:18).
- Why it's built this way: exposing
InterceptsLinksmeans the browser head keeps native anchor semantics (middle-click, open-in-new-tab) while only WebView heads pay the interop cost (ADR-042). - Where it's used: implemented by
MauiExternalLinkService,BrowserExternalLinkService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:98), and the defaultNullExternalLinkService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:47); consumed by theExternalLinkcomponent (its fake counterpartFakeExternalLinkServicebacks the component tests) and composed over byBrowserMapNavigationService.
IScreenshotService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/IScreenshotService.cs:8· Level 0 · interface
- What it is: captures the current app screen to a temporary image file, for pairing with a share action ("share my schedule as image").
- Depends on: BCL only; pairs with
IShareService.ShareFileAsync, which the doc comment names directly (IScreenshotService.cs:5). - Concept, permissionless temp-file capture.
[Rubric §26, Front-End Security]and[Rubric §30, Compliance/Privacy/Data Governance]. Captured files land in the platform cache directory, never the photo library, so no storage permissions are needed (IScreenshotService.cs:5-6), a deliberate minimization that avoids prompting for (and holding) a broad permission for a one-off share. - Walkthrough
IsSupported(IScreenshotService.cs:11): whether screen capture is available (web and null fallbacks reportfalse).CaptureToFileAsync(CancellationToken = default)(IScreenshotService.cs:14): captures the screen to a temp PNG and returns its path, ornullon failure.
- Why it's built this way: writing to the cache directory (not the gallery) keeps the feature permission-free; returning a nullable path lets the share flow abort quietly when capture is unsupported or fails.
- Where it's used: implemented by
MauiScreenshotServiceand the unsupportedNullScreenshotService, the TryAdd default (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:51); its output is handed toIShareService.ShareFileAsync.
IShareService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/IShareService.cs:8· Level 0 · interface
- What it is: opens the platform share affordance (a native share sheet on MAUI,
navigator.sharein browsers) for either a link or a local file (IShareService.cs:3-7). - Depends on: nothing first-party in its signature;
System.UriandSystem.Threading.CancellationTokenfrom the BCL. Its documented fallback partner is IClipboardService. - Concept introduced: share with a copy-link fallback. Both methods return
boolrather thanvoid, and the contract says the boolean means "was a share UI presented" (IShareService.cs:10,:14-16). That is what makes a Share button safe to render everywhere: when the answer isfalsethe caller copies the link through IClipboardService and confirms with a toast instead. It is also what makes theboolreturn on the clipboard contract useful, the two capabilities are designed as a pair.[Rubric §18, UI Architecture]§18 assesses whether the shared component tree stays host-agnostic. OneIShareServicecall site produces a native sheet, a browser share dialog, or a clipboard copy, with no host branching in markup.[Rubric §22, Responsive / Cross-Browser]§22 assesses graceful behavior across the host matrix.navigator.shareis absent on most desktop browsers, and the boolean return turns that absence into a designed path rather than a caught exception.
- Walkthrough: two members.
ShareLinkAsync(string title, Uri uri, CancellationToken = default)(IShareService.cs:11): shares a link with an accompanying title; returns whether a share UI was presented.ShareFileAsync(string title, string filePath, string contentType, CancellationToken = default)(IShareService.cs:17): shares a local file (a screenshot, for example); browser implementations reportfalsebecause they have no local file access (IShareService.cs:13-16).
- Why it's built this way: sharing is the capability with the widest spread between heads (full sheet, partial web support, none at all), so the contract encodes the failure as a return value instead of an exception and lets one shared component degrade deterministically (ADR-042).
- Where it's used: registered with the NullShareService default in
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:41), overridden by BrowserShareService on web heads (DependencyInjection.cs:89) and by MauiShareService on native ones. It consumes IScreenshotService output for image sharing and pairs with IClipboardService as its fallback.
NullClipboardService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Interop/NullClipboardService.cs:4· Level 1 · class (sealed)
- What it is: the default
IClipboardService: the clipboard is unavailable, and the call reports failure rather than pretending success (NullClipboardService.cs:3). - Depends on:
IClipboardService; BCLTask. - Concept: the boolean outcome return, as opposed to the boolean
IsSupportedprobe used by most siblings. This contract has no probe: a copy either worked or it did not, and the caller reacts to the answer it gets. Returningfalseis what lets a component show "copy failed, select the text manually" instead of a misleading "copied" toast.[Rubric §24, Forms / Validation / UX Safety]assesses whether the UI tells the truth about what happened. Reporting the failure keeps the user informed rather than silently discarding their action.
- Walkthrough: one member.
SetTextAsync(string text, CancellationToken)(NullClipboardService.cs:7-8) returnsTask.FromResult(false). - Why it's built this way: ADR-042. Clipboard write is permission-gated and can fail on real hosts too (a non-secure browser context, for example), so the contract already has a failure path and the null default simply takes it always.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:51); overridden byBrowserClipboardService(DependencyInjection.cs:106) andMauiClipboardService(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:49). Covered inCapabilityFallbackTests(CapabilityFallbackTests.cs:32-34).
NullExternalLinkService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Interop/NullExternalLinkService.cs:7· Level 1 · class (sealed)
- What it is: the default
IExternalLinkService: no link interception, so components render plain anchors withtarget="_blank", which the class summary notes is the correct behavior on web heads even without JavaScript (NullExternalLinkService.cs:3-6). - Depends on:
IExternalLinkService; BCLUriandTask. - Concept introduced, the flag that is a routing switch rather than a capability probe.
InterceptsLinksdoes not mean "I can open links"; it means "route clicks through me instead of letting the browser handle them" (MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Interop/IExternalLinkService.cs:11-16). The sharedExternalLinkcomponent reads it and builds anEventCallbackonly when it is true, otherwise leaving the click handler asdefaultso the plainMudLinkwithtarget="_blank"andrel="noopener noreferrer"handles the navigation natively (MMCA.Common/Source/Presentation/MMCA.Common.UI/Components/Capabilities/ExternalLink.razor:10-15,:31-34). With this default in place,OpenAsyncis therefore never called, which is why its inert body is harmless.[Rubric §25, Navigation & IA]assesses whether navigation stays coherent across surfaces. This flag exists becausetarget="_blank"silently dead-ends inside a WebView, so the native head must intercept while the web head must not (IExternalLinkService.cs:3-8).[Rubric §26, Front-End Security]assesses outbound-link hygiene. Therel="noopener noreferrer"that protects the opener lives in the component, not here, and it is exactly the path this default leaves in charge (ExternalLink.razor:14).
- Walkthrough
InterceptsLinks(NullExternalLinkService.cs:10): constantfalse, the "let the anchor do its job" answer.OpenAsync(Uri uri, CancellationToken)(NullExternalLinkService.cs:13): returnsTask.CompletedTask, dropping the URI. Reached only if a caller ignoresInterceptsLinksand calls it anyway.
- Why it's built this way: ADR-042. The framework routes every external link through one component so the WebView problem is solved once; on hosts without that problem the cheapest correct behavior is the browser's own.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:56); overridden byBrowserExternalLinkService(DependencyInjection.cs:107) and byMauiExternalLinkService(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:54), which is the implementation that actually setsInterceptsLinksto true.CapabilityFallbackTestsasserts the flag is false andOpenAsyncdoes not throw (CapabilityFallbackTests.cs:156,:156).
NullScreenshotService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Interop/NullScreenshotService.cs:4· Level 1 · class (sealed)
- What it is: the default
IScreenshotService: screen capture unavailable (NullScreenshotService.cs:3). - Depends on:
IScreenshotService; BCLTask. - Concept: probe-plus-nullable-result, the same pairing as
NullBarcodeScannerService:IsSupportedsteers the UI and the operation still answers safely for a caller that ignores it. The returnedstring?is a file path, sonullmeans "no file was written" and there is nothing for the caller to clean up. - Walkthrough
IsSupported(NullScreenshotService.cs:7): constantfalse.CaptureToFileAsync(CancellationToken)(NullScreenshotService.cs:10-11): returnsTask.FromResult<string?>(null).
- Why it's built this way: ADR-042. Capturing the screen is a native-only operation with privacy weight, and there is no browser equivalent the framework wires, so this default is what every web head runs.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:60);MauiScreenshotServiceis the only override (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:58). Covered inCapabilityFallbackTests(CapabilityFallbackTests.cs:151).
NullShareService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Interop/NullShareService.cs:4· Level 1 · class (sealed)
- What it is: the default
IShareService: the platform share sheet is unavailable, and the summary names the intended consequence, callers fall back to copy-link (NullShareService.cs:3). - Depends on:
IShareService; BCLUriandTask. - Concept: a fallback whose
falsereturn is a hand-off signal between two capabilities. A share button that getsfalsefromShareLinkAsyncis expected to copy the URL throughIClipboardServiceinstead, so the two contracts compose into one user-visible affordance that behaves sensibly wherever it lands, including on a host where both are null (there the copy also reports failure and the UI says so).[Rubric §18, UI Architecture]assesses how presentation decisions are layered. The share-or-copy choice lives in the component, driven by a returned outcome rather than by host detection.
- Walkthrough
ShareLinkAsync(string title, Uri uri, CancellationToken)(NullShareService.cs:7-8):Task.FromResult(false).ShareFileAsync(string title, string filePath, string contentType, CancellationToken)(NullShareService.cs:11-12):Task.FromResult(false). Note it takes a path plus a MIME type rather than a stream, so the null implementation owns no resource and has nothing to dispose.
- Why it's built this way: ADR-042.
navigator.shareis not universally available even in browsers, so the contract already had to model "sharing did not happen" as an ordinary outcome; the null default is that outcome, always. - Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:50); overridden byBrowserShareService(DependencyInjection.cs:105) andMauiShareService(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:48). Both members are asserted false inCapabilityFallbackTests(CapabilityFallbackTests.cs:20-27).
BrowserClipboardService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/BrowserClipboardService.cs:4· Level 2 · class (sealed)
- What it is: the web adapter for
IClipboardService, writing text to the system clipboard throughnavigator.clipboard.writeText. - Depends on:
IClipboardServiceandCapabilitiesJsModule(MMCA.Common.UI/Services/Capabilities/Interop/BrowserClipboardService.cs:6,:9); thecopyTextexport (MMCA.Common.UI/wwwroot/capabilities-interop.js:25). - Concept introduced: the group's tri-state to bool collapse, worth stating once because several adapters repeat it.
CapabilitiesJsModulereturnsbool?:true(the browser did it),false(the browser refused), ornull(JS never ran). Adapters whose contract returnsboolcollapse the last two with== true, so "not attempted" and "attempted and failed" are equally honest answers to "did the copy land".[Rubric §18, UI Architecture]assesses whether the UI can tell the user the truth about what happened; the boolean is what lets a caller show a "copied" snackbar only on a real success. - Walkthrough: the constructor captures the module (
MMCA.Common.UI/Services/Capabilities/Interop/BrowserClipboardService.cs:9).SetTextAsync(string text, CancellationToken = default)(:12-18) invokescopyTextwith the text (:14-16) and returnscopied == true(:17). On the JS side,copyTextreturnsfalseup front whennavigator.clipboardis absent (an insecure context or an older browser) and catches a rejectedwriteText(a denied permission) tofalse(MMCA.Common.UI/wwwroot/capabilities-interop.js:26-34). - Why it's built this way: the Clipboard API is permission-gated and unavailable over plain HTTP, so a
boolreturn rather than a throw lets the copy-link affordance simply do nothing visible where it is unsupported. - Where it's used: registered scoped as
IClipboardServicebyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:97); siblings areMauiClipboardServiceandNullClipboardService. It is the copy-link fallback forIShareServicecallers, usually paired withIPublicLinkBuilderto produce the URL.
BrowserExternalLinkService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/BrowserExternalLinkService.cs:8· Level 2 · class (sealed)
- What it is: the web adapter for
IExternalLinkService. On the web an anchor already works, so it reportsInterceptsLinks => falseand only handles programmatic opens (the maps fallback) throughwindow.open. - Depends on:
IExternalLinkServiceandCapabilitiesJsModule(MMCA.Common.UI/Services/Capabilities/Interop/BrowserExternalLinkService.cs:10,:13); theopenExternalexport (MMCA.Common.UI/wwwroot/capabilities-interop.js:37); BCLUri. - Concept introduced: the
InterceptsLinkscapability flag, the group's answer to "should the component render a plain anchor or route through the service". A native head inside aBlazorWebViewmust intercept, becausetarget="_blank"dead-ends there; a browser head must not, because intercepting would replace working native anchor behavior (middle-click, open-in-new-tab, the browser's own popup policy) with a worse imitation (:3-6).[Rubric §25, Navigation & IA]assesses whether navigation is coherent and predictable across the app; one flag lets a single shared link component be correct on both heads.[Rubric §26, Front-End Security]applies to the JS side:window.open(url, '_blank', 'noopener,noreferrer')(MMCA.Common.UI/wwwroot/capabilities-interop.js:39) prevents the opened page from reaching back throughwindow.openerand strips the referrer. - Walkthrough: the constructor captures the module (
MMCA.Common.UI/Services/Capabilities/Interop/BrowserExternalLinkService.cs:13).InterceptsLinks => false(:16).OpenAsync(Uri uri, CancellationToken = default)(:19-26) null-guardsuri(:21) and invokesopenExternalwithuri.ToString()(:23-25), discarding the result because the contract returnsTask. - Why it's built this way: leaving anchors alone on the web is the cheaper and more correct default; the programmatic path exists only for callers like
BrowserMapNavigationServicethat have no anchor to click. - Where it's used: registered scoped as
IExternalLinkServicebyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:98); siblings areMauiExternalLinkServiceandNullExternalLinkService. Consumed by the shared external-link component and byBrowserMapNavigationService. - Caveats / not-in-source: a popup blocker can make
window.openreturnnullwhile the JS still reportstrue(MMCA.Common.UI/wwwroot/capabilities-interop.js:37-44only catches a throw), so an open silently blocked by the browser is not detected.
BrowserShareService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Interop·MMCA.Common.UI/Services/Capabilities/Interop/BrowserShareService.cs:8· Level 2 · class (sealed)
- What it is: the web adapter for
IShareService, opening the browser's native share sheet through the Web Share API for links, and declining file sharing outright. - Depends on:
IShareServiceandCapabilitiesJsModule(MMCA.Common.UI/Services/Capabilities/Interop/BrowserShareService.cs:10,:13); theshareLinkexport (MMCA.Common.UI/wwwroot/capabilities-interop.js:12); BCLUri. - Concept introduced, partial capability implementation, the third posture in this group alongside "real" and "inert". Web Share exists but covers only part of the contract, so one method is genuinely implemented and the other returns a hard
falserather than pretending. That honestfalseis what lets a Share button on desktop Firefox fall back to copy-link instead of appearing to work and doing nothing.[Rubric §22, Responsive/Cross-Browser]assesses behavior across browsers that do not agree on a feature; the class doc names the exact gaps (desktop Firefox, insecure contexts,:4-6).[Rubric §18, UI Architecture]applies because the boolean return is the contract that makes a fallback affordance possible at all. - Walkthrough
- The constructor captures the module (
MMCA.Common.UI/Services/Capabilities/Interop/BrowserShareService.cs:13). ShareLinkAsync(string title, Uri uri, CancellationToken = default)(:16-24): null-guardsuri(:18), invokesshareLinkwith the title and the URI string (:20-22), and returnsshared == true(:23). On the JS side,shareLinkreturnsfalsewhennavigator.shareis absent and catches the dismissal and permission rejections (AbortError,NotAllowedError) tofalse(MMCA.Common.UI/wwwroot/capabilities-interop.js:13-22), so a user who closes the share sheet is reported as "not shared".ShareFileAsync(string title, string filePath, string contentType, CancellationToken = default)(:27-28):Task.FromResult(false), unconditionally. A browser has no local file path to share.
- The constructor captures the module (
- Why it's built this way: collapsing "user dismissed" into
falsealongside "unsupported" is the same simplification the clipboard adapter makes, and it is right here because both outcomes mean the link was not shared. Returning a constantfalsefor files, rather than throwingNotSupportedException, keeps the contract uniform so callers branch on a value instead of catching. - Where it's used: registered scoped as
IShareServicebyAddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:96); siblings areMauiShareServiceandNullShareService, withIClipboardServiceas the fallback path andIPublicLinkBuildersupplying the URL.
IBarcodeScannerService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common.UI/Services/Capabilities/Media/IBarcodeScannerService.cs:11· Level 0 · interface
- What it is: opens the device camera to scan a QR code or barcode and hands back the first decoded
payload, or
nullwhen the scan did not produce one. - Depends on: BCL only (
System.Threading.Tasks.Task,System.Threading.CancellationToken). - Concept introduced, the affordance switch (as opposed to a degraded path). Most contracts in this
group have a plausible web story, so their browser adapter does a lesser version of the same thing.
Camera scanning has none: there is no browser primitive the framework wraps, so web heads keep the
inert default and the UI hides the scan affordance entirely rather than offering a control that
cannot work. The doc comment names this distinction explicitly, "the affordance switch, not a degraded
path" (
IBarcodeScannerService.cs:6-9), andIsSupportedis the switch components read.[Rubric §18, UI Architecture]assesses whether shared components can serve several hosts without branching on host type; a capability boolean plus a hidden affordance is that branch expressed as data instead of#ifor host sniffing.[Rubric §26, Front-End Security]assesses how untrusted client-side input is handled. The contract states the rule in the contract itself: "the scanned payload is untrusted input: validate it before acting on it" (IBarcodeScannerService.cs:9). A camera can decode any code that is pointed at it, so the decoded string is attacker-supplied by construction.[Rubric §29, Resilience & Business Continuity]assesses graceful degradation. The implementations "never throw": a denied camera permission, a cancelled scan, an unsupported head, and a cancelled token all collapse to onenull(IBarcodeScannerService.cs:4-7), so the caller has exactly one failure shape to handle.
- Walkthrough
IsSupported(IBarcodeScannerService.cs:14): whether camera scanning is available on this head. False on web heads and on any native head that did not opt in (see below).ScanAsync(CancellationToken = default)(IBarcodeScannerService.cs:20): opens the camera scanner and returns the first decoded payload, ornullwhen cancelled, denied, or unavailable (IBarcodeScannerService.cs:16-19). One call, one payload: the contract is a modal "scan one thing" operation, not a continuous decode stream, which is what keeps it expressible as a plainTask.
- Why it's built this way: ADR-042 puts every device capability behind an interface, and the
registration for this one is doubly conservative. The framework default is the inert
NullBarcodeScannerService, registered withTryAddSingleton(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:70), and the comment above it records that even a MAUI head keeps that default "until it asks for the camera" (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:67-69). The native adapter arrives only through the opt-inUseCommonBarcodeScanner(...)builder extension (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:104-115), which registers the ZXing.Net.MAUI handlers withUseBarcodeReader()and overrides the registration with a factory that buildsMauiBarcodeScannerService(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:111-113). It is deliberately not folded intoUseMauiDeviceCapabilitiesso that "a head that never scans should ship neither the camera handler nor a camera permission declaration" (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:86-87), a privacy and store-review consideration as much as a size one. The opt-in takes the scan page's cancel and camera-description text as twoFunc<string>delegates (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:104-106), invoked once per scan when the page is built so the modal follows the user's in-app language choice rather than the device language that was active at startup (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:88-95). The head still declares the platform permission itself (Android CAMERA, iOS NSCameraUsageDescription) and must call the opt-in afterAddUISharedso the plain Add beats the TryAdd default (MMCA.Common.UI.Maui/HostingDependencyInjection.cs:96-100). - Where it's used: implemented by
MauiBarcodeScannerService(which drivesBarcodeScanPage) and the inertNullBarcodeScannerService; consumed by ADC's QR check-in scan page (CheckInScan).
ISpeechToTextService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common.UI/Services/Capabilities/Media/ISpeechToTextService.cs:10· Level 0 · interface
- What it is: the capability contract for dictating speech into text fields (feedback forms, live Q&A questions) through the platform recognizer (
ISpeechToTextService.cs:5-9). Like every contract in this group it is a tiny, platform-free interface that the shared component tree depends on instead of naming a MAUI or browser recognizer type. - Depends on: nothing first-party. It speaks in
System.Globalization.CultureInfo,System.IProgress<string>,System.Threading.CancellationToken, andTask<string?>only, soMMCA.Common.UIcarries no dependency on any platform recognizer. - Concept introduced: the contract-per-capability shape and the two-phase last-wins registration are established by this chapter's overview and by IFormFactor; this type reuses them for speech input. The house rule worth naming here is the
IsSupportedgate as an affordance switch, not a degraded path: web and null fallbacks reportIsSupportedfalse(ISpeechToTextService.cs:7-8) and shared components hide the microphone button entirely rather than offering one that silently fails.[Rubric §22, Responsive / Cross-Browser]§22 assesses whether the app adapts across the device and host matrix. Speech input is native-only here, and the interface makes that variance a single injected boolean instead of a compile-time switch in component markup.[Rubric §21, Accessibility]§21 assesses inclusive input and output paths. Dictation is an accessibility affordance for text entry, offered where the platform supports it and cleanly hidden where it does not.
- Walkthrough: two members.
IsSupported(ISpeechToTextService.cs:13): whether speech recognition is available on this platform.ListenAsync(CultureInfo, IProgress<string>?, CancellationToken)(ISpeechToTextService.cs:20-23): listens until the recognizer finalizes or the token cancels, streaming partial hypotheses through thepartialResultsprogress sink, and returns the final transcript, ornullon permission denial, cancellation, or recognizer failure (ISpeechToTextService.cs:16-19). The null-on-failure contract is the never-throw discipline the whole layer follows: a caller can only fall back to typing, never to a broken path.
- Why it's built this way: keeping recognition behind an interface that names no platform type lets the MAUI head supply a real recognizer while browser and null heads register an inert one, all selected at DI composition time (ADR-042,
Website/docs-src/adr/042-device-capability-abstraction.md). - Where it's used: registered as a singleton with the NullSpeechToTextService default in
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:54); the native override is MauiSpeechToTextService in theMMCA.Common.UI.Mauipackage. Consumed by shared components that offer voice input on feedback and Q&A forms.
ITextToSpeechService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common.UI/Services/Capabilities/Media/ITextToSpeechService.cs:9· Level 0 · interface
- What it is: the output counterpart to ISpeechToTextService, reading text aloud (session descriptions, announcements) through the platform speech synthesizer and matching the active UI culture's voice when one is installed (
ITextToSpeechService.cs:3-8). - Depends on: nothing first-party;
System.Threading.CancellationTokenandTaskonly. - Concept introduced: nothing new. It applies the same
IsSupportedaffordance switch and never-throw contract as its dictation sibling: web and null fallbacks reportIsSupportedfalseand components hide the affordance (ITextToSpeechService.cs:6-8).[Rubric §21, Accessibility]§21 assesses inclusive output. Read-aloud is an accessibility affordance offered wherever the platform can synthesize speech.[Rubric §27, i18n]§27 assesses localization depth. The contract documents culture-matched voice selection with an explicit fall back to the default voice when no voice matches the current culture (ITextToSpeechService.cs:14-18).
- Walkthrough: three members.
IsSupported(ITextToSpeechService.cs:12): whether synthesis is available.SpeakAsync(string, CancellationToken)(ITextToSpeechService.cs:19): speaks the text and completes when playback ends; cancel the token or callStopAsyncto interrupt.StopAsync()(ITextToSpeechService.cs:22): stops any in-progress speech.
- Why it's built this way: same rationale as the dictation contract, one narrow capability behind a platform-free interface, real on MAUI and inert elsewhere (ADR-042).
- Where it's used: registered with the NullTextToSpeechService default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:48); MauiTextToSpeechService overrides it on native heads. Consumed by components that read session and announcement text aloud.
PickedMedia
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:29· Level 0 · class
- What it is: the framework-owned result of a photo pick or capture: a stream plus its file name and MIME type, returned by IMediaPickerService (
IMediaPickerService.cs:21-28). - Depends on: nothing first-party;
System.IO.StreamandSystem.IDisposable. - Concept introduced: this is the one capability payload in the layer that is deliberately a class, not a record, and the reason is a concrete AOT constraint worth knowing. A record's compiler-generated
IEquatable<T>is a generic WinRT interface, which trips CsWinRT AOT generation (CsWinRT1030) on the windows TFM ofUI.Maui(IMediaPickerService.cs:22-25). So where every other payload here is arecord, this one is asealed classwith get-only properties.[Rubric §15, Best Practices & Code Quality]§15 assesses idiomatic, toolchain-aware code. The deviation from the record convention is documented in place with the exact analyzer id, so the next reader does not "fix" it back into a record and break the MAUI windows build.[Rubric §12, Performance & Scalability]§12 assesses resource handling. The type owns aStreamand implementsIDisposable, so callers dispose after upload rather than holding image bytes open.
- Walkthrough: a primary-constructor
sealed classimplementingIDisposable(IMediaPickerService.cs:29).Content(:32): the photo bytes, positioned at the start.FileName(:35) andContentType(:38): the original or generated file name and the platform-reported MIME type.Dispose()(:41): disposes the underlying stream.
- Why it's built this way: keeping the picked-photo shape as a framework type (rather than a MAUI
FileResult) lets a shared avatar-upload component consume it identically on every head, while the class-over-record choice keeps the native windows AOT build green (ADR-045 for media picking, ADR-042 for the layer). - Where it's used: the return type of
IMediaPickerService.PickPhotoAsync/CapturePhotoAsync(MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:15,:18), produced on native heads by MauiMediaPickerService; shared avatar-upload UI consumes the stream and disposes it.
IMediaPickerService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:9· Level 1 · interface
- What it is: picks or captures a photo on native heads (avatar upload), returning PickedMedia or
null(IMediaPickerService.cs:3-8). Implementations own the photo-library and camera permission flow and never throw. - Depends on: PickedMedia, its result type;
System.Threading.CancellationToken. - Concept introduced: the clearest statement of the layer's affordance switch, not degraded path idea. Web heads keep the null default and render a plain
InputFileinstead, and the doc comment names this "the affordance switch, not a degraded path" (IMediaPickerService.cs:6-7): the browser does not attempt a broken native picker, it presents a different control that works.[Rubric §18, UI Architecture]§18 assesses host-agnostic componentization. A shared avatar component branches onIsSupportedbetween the native picker andInputFile, keeping one component tree across heads.
- Walkthrough: three members.
IsSupported(IMediaPickerService.cs:12): whether native photo picking is available on this head.PickPhotoAsync(CancellationToken)(:15): opens the photo picker; returnsnullwhen cancelled or unavailable.CapturePhotoAsync(CancellationToken)(:18): opens the camera; returnsnullwhen cancelled, denied, or unavailable.
- Why it's built this way: media picking is native-only, so it hides behind a platform-free contract with an inert default and a MAUI override (ADR-045 for media picking, ADR-042 for the layer).
- Where it's used: registered with the NullMediaPickerService default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:65), overridden by MauiMediaPickerService; consumed by the shared avatar-upload UI, which disposes the returned PickedMedia stream after upload.
NullBarcodeScannerService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Media/NullBarcodeScannerService.cs:9· Level 1 · class (sealed)
- What it is: the no-op camera scanner for
IBarcodeScannerService. Browsers have no shared camera-scanning primitive the framework can rely on, so web heads keep this default and simply hide the scan button, and the native override ships inMMCA.Common.UI.Mauias an opt-in (NullBarcodeScannerService.cs:3-8). - Depends on:
IBarcodeScannerService; BCLTask. - Concept introduced, the default that survives even on the capable host. Most fallbacks in this unit are replaced automatically on the head that can do the job. This one is not:
UseMauiDeviceCapabilities()deliberately leaves it alone (noIBarcodeScannerServiceregistration appears inMMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:42-62), and a MAUI head keeps this null scanner until it separately callsUseCommonBarcodeScanner()(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/HostingDependencyInjection.cs:104-113), because pulling in the camera adapter also means shipping a camera permission declaration (DependencyInjection.cs:76-79). "Unsupported" here therefore encodes two different situations behind one flag: the platform cannot scan, or the app chose not to.[Rubric §22, Responsive / Cross-Browser]assesses graceful degradation across heads. TheIsSupportedflag is what lets one shared page render a scan button on a phone and omit it in a browser without host detection.[Rubric §11, Security]assesses least-privilege posture. Keeping the camera adapter opt-in means an app that never scans requests no camera permission at all.
- Walkthrough
IsSupported(NullBarcodeScannerService.cs:12): constantfalse; components hide the affordance.ScanAsync(CancellationToken)(NullBarcodeScannerService.cs:15-16): returnsTask.FromResult<string?>(null), ignoring the token. That is deliberate and pinned by a test: even a pre-cancelled token must come back as a plainnullrather than anOperationCanceledException, because the contract is that the scan affordance is simply absent, not that a scan was interrupted (CapabilityFallbackTests.cs:129-143).
- Why it's built this way: ADR-042. A cancelled-token exception would force every caller into a
try/catchfor a case that is not an error, so the null result carries both "no camera" and "no scan happened" uniformly. - Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:79), registered last among the singletons and under its own explanatory comment; the opt-in native override isMauiBarcodeScannerService. Covered inCapabilityFallbackTests(CapabilityFallbackTests.cs:129-143).
NullSpeechToTextService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Media/NullSpeechToTextService.cs:6· Level 1 · class (sealed)
- What it is: the default
ISpeechToTextService: no recognizer, so components hide the microphone (NullSpeechToTextService.cs:5). - Depends on:
ISpeechToTextService; externalsSystem.Globalization.CultureInfo(NullSpeechToTextService.cs:1) and BCLIProgress<T>. - Concept: the fallback for a streaming contract.
ListenAsynctakes anIProgress<string>?for interim transcripts alongside the final text it returns. The null implementation simply never invokes the progress reporter and returnsnull, so a caller bound to partial results receives nothing at all rather than an empty-string flicker, and the finalnullreads as "nothing was recognized".[Rubric §21, Accessibility]assesses alternative input paths. Voice entry is an accessibility affordance, and this default is where it is absent, which is whyIsSupportedexists to hide the microphone rather than leaving a dead button on the form.
- Walkthrough
IsSupported(NullSpeechToTextService.cs:9): constantfalse.ListenAsync(CultureInfo culture, IProgress<string>? partialResults, CancellationToken)(NullSpeechToTextService.cs:12-16): returnsTask.FromResult<string?>(null), ignoring the requested recognition culture and never calling back intopartialResults.
- Why it's built this way: ADR-042. Speech recognition is a platform service with a microphone permission attached, so an unavailable recognizer must be an ordinary, prompt-free
nullrather than an exception a form has to handle. - Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:63);MauiSpeechToTextServiceis the native override (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:62). ADC registers it explicitly in a bUnit test so the live-channel page renders without a recognizer (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/LiveChannelJoinTests.cs:61-62), andCapabilityFallbackTestspins thenullresult (CapabilityFallbackTests.cs:152-153).
NullTextToSpeechService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Media/NullTextToSpeechService.cs:4· Level 1 · class (sealed)
- What it is: the default
ITextToSpeechService: no synthesizer, so components hide the affordance (NullTextToSpeechService.cs:3). - Depends on:
ITextToSpeechService; BCLTask. - Concept: the read-aloud counterpart to
NullSpeechToTextService, and the one place in this unit where a stop operation has to stay safe.StopAsynccompletes normally even though nothing was ever speaking, because the caller's teardown path (a component disposing while audio might be playing) must not need to know whether playback ever started.[Rubric §21, Accessibility]assesses alternative output paths. Read-aloud is distinct from the screen-reader channel behindIAccessibilityAnnouncer: this one is user-invoked content playback, and it is simply absent on hosts that keep this default.
- Walkthrough
IsSupported(NullTextToSpeechService.cs:7): constantfalse.SpeakAsync(string text, CancellationToken)(NullTextToSpeechService.cs:10):Task.CompletedTask, discarding the text.StopAsync()(NullTextToSpeechService.cs:13):Task.CompletedTask. It takes no cancellation token, matching the contract: stopping is itself the cancellation.
- Why it's built this way: ADR-042. Synthesis is a platform service with no framework-wired browser equivalent, and the affordance is an enhancement, so the inert default costs the feature nothing but the button.
- Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:57);MauiTextToSpeechServiceis the native override (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:55).CapabilityFallbackTestsassertsIsSupportedis false and that both calls are non-throwing (CapabilityFallbackTests.cs:155,:153-154).
NullMediaPickerService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Media·MMCA.Common.UI/Services/Capabilities/Media/NullMediaPickerService.cs:7· Level 2 · class (sealed)
- What it is - the no-op default for
IMediaPickerService(avatar photo pick and capture). It reports the native picker unavailable and returnsnullfrom both operations, which for web heads means "render a plainInputFileinstead," not a degraded experience (NullMediaPickerService.cs:3-6). - Depends on - implements
IMediaPickerService; returnsPickedMedia?. Same null-object shape as the siblings above. - Concept introduced - the affordance switch, not a degraded path. Unlike geocoding, which simply vanishes when unsupported, media picking has a full web alternative: the browser's own file input. So
IsSupported == falsehere does not mean "you cannot upload a photo," it means "do not draw the native picker button; draw the standard file input instead." The contract itself says so in as many words (MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:6-7), and the null default's only job is to signal that switch. Same [Rubric §2 - Design Patterns] and [Rubric §1 - SOLID] framing asNullGeocodingService; additionally [Rubric §18 - UI Architecture], which assesses how cleanly presentation concerns are separated, is visible here because the choice between native picker and file input is driven by a resolved capability rather than by host-detection code inside the component. - Walkthrough -
sealed class(NullMediaPickerService.cs:7).IsSupported => false(NullMediaPickerService.cs:10).PickPhotoAsync(NullMediaPickerService.cs:13-14) andCapturePhotoAsync(NullMediaPickerService.cs:17-18) both returnTask.FromResult<PickedMedia?>(null). Returningnullrather than an emptyPickedMediamatters for ownership:PickedMediaisIDisposableand owns aStreamthe caller must dispose after upload (MMCA.Common.UI/Services/Capabilities/Media/IMediaPickerService.cs:21-22,29,41), so a fallback that handed back an instance would be handing back a disposal obligation for nothing. - Why it's built this way - ADR-045 (managed file storage and avatars), cited directly in the class summary (
NullMediaPickerService.cs:4). Web avatar upload rides on the browser file input, so the native picker abstraction exists only to give MAUI heads a camera and photo-library flow; making the default inert keeps the shared avatar surface host-agnostic. - Where it's used - registered by
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:65); overridden byMauiMediaPickerServiceon native heads (MMCA.Common.UI.Maui/DependencyInjection.cs:71). ADC's profile page is the worked consumer: it injects the contract (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.UI/Pages/Users/Profile/Profile.razor.cs:22), routes its pick and capture actions through it (Profile.razor.cs:96,98), and gates the native buttons onMediaPicker.IsSupported, falling through to anInputFileelement when the default is resolved (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.UI/Pages/Profile/Profile.razor:23,47,60). Web and Server heads keep this default.
LocalNotificationRequest
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/LocalNotificationRequest.cs:17· Level 0 · record
- What it is: the framework-owned value type describing one scheduled local (on-device) notification, the payload passed to ILocalNotificationService (
LocalNotificationRequest.cs:5-8). It plays the same role for notifications that GeoPoint, PickedMedia, and PushDeviceToken play for their capabilities: it keeps a platform type out of the shared contract. - Depends on: nothing first-party; positional parameters of
int,string, andSystem.DateTimeOffsetonly. - Concept introduced: the stable-id-as-idempotency-key convention for on-device scheduling. The
Idmust be stable per logical subject (a hash of a session id, for example) so that rescheduling replaces rather than duplicates the pending entry (LocalNotificationRequest.cs:6-7,:7). It is the local, offline analogue of the server-side idempotency key.[Rubric §9, API & Contract Design]§9 assesses well-shaped contracts. This is a small, documentedsealed recordwhose XML comments pin the meaning of every field: id stability, past-dated delivery being ignored, the optional deep-link route.
- Walkthrough: a single positional
sealed record(LocalNotificationRequest.cs:17-22) with five members.Id(:16, documented at:7): the stable platform notification id; scheduling the same id replaces the pending entry.Title/Body(:17-18, documented at:8-9): already localized by the caller, the record does no i18n itself.DeliverAt(:19, documented at:10): absolute delivery time; requests in the past are ignored.DeepLinkRoute(:20, documented at:11-14): an optional app-relative route (for example/conference/sessions/42) published to IDeepLinkDispatcher when the user taps the notification, which is how a reminder wires back into Blazor routing.
- Why it's built this way: a
recordgives value equality and immutability for free, and keeping it in the shared UI layer means the notification-scheduling contract never references a platform notification builder (ADR-042). - Where it's used: the parameter of
ILocalNotificationService.ScheduleAsync(MMCA.Common.UI/Services/Capabilities/Notifications/ILocalNotificationService.cs:22); MauiLocalNotificationService translates it into a native scheduled notification. - Caveats / not-in-source: the record documents that past-dated requests are ignored, but that enforcement lives in the platform implementation, not in this record. On MAUI it is an early return in
ScheduleAsyncfor anyDeliverAtat or before now (MMCA.Common.UI.Maui/Capabilities/Notifications/MauiLocalNotificationService.cs:42-45).
PushDeviceToken
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/IPushDeviceTokenProvider.cs:19· Level 0 · record
- What it is: a platform push handle, the wire platform value plus the device token, returned by IPushDeviceTokenProvider (
IPushDeviceTokenProvider.cs:16-19). It is the framework-owned value type that keeps FCM and APNs specifics out of the shared registration pipeline. - Depends on: nothing first-party; two
stringpositional parameters. - Concept introduced: nothing new structurally, it is one more shared-UI value record like GeoPoint and LocalNotificationRequest. What is worth noting is the deliberately narrow wire vocabulary:
Platformis documented as one offcmv1orapns(IPushDeviceTokenProvider.cs:17), so the whole push path speaks two stable string values rather than a platform enum that would have to be versioned across the wire.[Rubric §9, API & Contract Design]§9 assesses contract clarity. The record pins the two-field push handle shape and documents the exact meaning of each field.
- Walkthrough: a two-field
sealed record(IPushDeviceTokenProvider.cs:19).Platform(documented at:17): the wire platform value,fcmv1orapns.Token(documented at:18): the FCM registration token or the APNs device token.
- Why it's built this way: modeling the handle as a shared record means the registration pipeline (IPushRegistrationService and the notification module) never references a Firebase or APNs type; the credentialed provider lives at the app edge (ADR-044).
- Where it's used: the return type of
IPushDeviceTokenProvider.GetTokenAsync(MMCA.Common.UI/Services/Capabilities/Notifications/IPushDeviceTokenProvider.cs:13); MauiPushRegistrationService forwards it to the backend.
IPushRegistrationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/IPushRegistrationService.cs:10· Level 0 · interface
- What it is: client-side orchestration of native push device registration: obtains the platform
token from
IPushDeviceTokenProviderand syncs it to the server's Devices endpoint. - Depends on:
IPushDeviceTokenProvider(named by the doc comment as the token source the implementations wrap,IPushRegistrationService.cs:4-5). - Concept, native push registration lifecycle.
[Rubric §6, CQRS & Event-Driven](the push channel is a delivery leg for notifications) and[Rubric §18, UI Architecture]. This is the client leg of native push delivery (ADR-044): hosts callRegisterAsyncafter sign-in and on resume, andUnregisterAsyncbefore sign-out clears the tokens, because the delete call is authenticated (IPushRegistrationService.cs:6-8). The default implementation is a no-op on web heads (IPushRegistrationService.cs:8). - Walkthrough
IsSupported(IPushRegistrationService.cs:13): whether this head can register for native push at all (native heads only).RegisterAsync(CancellationToken = default)(IPushRegistrationService.cs:20): registers or refreshes this device's installation; best-effort and safe to call repeatedly; returnsfalsewhen no platform token is available (unsupported head, missing credentials, permission denied) or the sync failed (IPushRegistrationService.cs:16-18).UnregisterAsync(CancellationToken = default)(IPushRegistrationService.cs:23): removes the installation, best-effort, called while still authenticated (IPushRegistrationService.cs:22).
- Why it's built this way: ordering
UnregisterAsyncbefore token clearing is load-bearing: the server delete call is authenticated, so it must run while the credentials still exist; the idempotent, safe-to-repeatRegisterAsynctolerates the resume-driven re-calls (ADR-044). - Where it's used: implemented by
MauiPushRegistrationService(overIPushDeviceTokenProvider) and the no-opNullPushRegistrationService. Both this contract and its token provider default to inert (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:61-62), and the comment above them records the split:MMCA.Common.UI.Mauioverrides the registration service while the app overrides the token provider once real FCM/APNs credentials exist, so until then even native heads stay registered-but-tokenless (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:58-60). Driven by the head apps' sign-in and sign-out lifecycle and syncing to the server's Devices endpoint (ADR-044).
ILocalNotificationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/ILocalNotificationService.cs:10· Level 1 · interface
- What it is: schedules on-device notifications (session reminders) with no backend involvement (
ILocalNotificationService.cs:3-9). It consumes LocalNotificationRequest and is a native-only capability. - Depends on: LocalNotificationRequest, its schedule payload;
System.Threading.CancellationTokenandIReadOnlyCollection<int>otherwise. - Concept introduced: the own-the-permission-flow, never-throw-on-denial discipline, made explicit here. Implementations own the platform permission flow (Android 13+
POST_NOTIFICATIONS, iOS notification authorization) and never throw on denial: scheduling simply becomes a no-op until permission is granted (ILocalNotificationService.cs:6-9,:21). This is distinct from the pureIsSupportedgate, because a supported platform can still be un-permissioned, and the contract makes that state safe.[Rubric §26, Front-End Security]§26 assesses permission-gated features. Notification permission is requested explicitly and its absence degrades to a silent no-op.[Rubric §24, Forms / Validation / UX Safety]§24 assesses safe state transitions. Rescheduling by stable id (replace, not duplicate) prevents notification spam from repeated schedules.
- Walkthrough: five members.
IsSupported(ILocalNotificationService.cs:13): whether this platform can schedule local notifications.RequestPermissionAsync(CancellationToken)(:19): ensures permission, prompting if the platform requires consent and it is undecided, and returns whether notifications are currently permitted.ScheduleAsync(LocalNotificationRequest, CancellationToken)(:22): schedules, or replaces by id, a pending notification; a no-op without permission.CancelAsync(IReadOnlyCollection<int>, CancellationToken)(:25): cancels pending notifications by id; unknown ids are ignored.CancelAllAsync(CancellationToken)(:28): cancels every pending notification scheduled by this app.
- Why it's built this way: on-device reminders need no server, so they are a pure native capability behind a platform-free interface with an inert default for web and server heads (ADR-042).
- Where it's used: registered with the NullLocalNotificationService default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:50); MauiLocalNotificationService implements real scheduling. The route from a tapped notification flows into IDeepLinkDispatcher via the request'sDeepLinkRoute, published by the package bootstrap rather than by the service itself (MMCA.Common.UI.Maui/Capabilities/Notifications/MauiLocalNotificationService.cs:12).
IPushDeviceTokenProvider
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/IPushDeviceTokenProvider.cs:10· Level 1 · interface
- What it is: supplies the platform push handle for this device, returning PushDeviceToken or
null(IPushDeviceTokenProvider.cs:3-9). Apps plug in their credentialed implementation: a Firebase messaging token on Android, an APNs device token on iOS. - Depends on: PushDeviceToken, its return shape;
System.Threading.CancellationToken. - Concept introduced: the inert-until-credentialed default. The out-of-box default returns
null, which keeps the whole registration pipeline inert until real push credentials exist (IPushDeviceTokenProvider.cs:6-9). Even a native MAUI head stays registered-but-tokenless until the app supplies a credentialed provider, which is stated again at the registration site (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:58-60), so no half-wired push path ships by accident.[Rubric §26, Front-End Security]§26 assesses credential handling. Push credentials are an app-owned edge concern; the framework contract carries no keys and stays inert without them.
- Walkthrough: one member.
GetTokenAsync(CancellationToken)(IPushDeviceTokenProvider.cs:13): the current platform token, ornullwhen unavailable. Implementations request notification permission as needed and never throw (:7-8).
- Why it's built this way: separating the token provider (app-owned, credentialed) from the registration service (framework-owned) means the framework ships a complete push pipeline that stays dormant until an app drops in real FCM or APNs credentials (ADR-044).
- Where it's used: registered with the NullPushDeviceTokenProvider default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:62); theMMCA.Common.UI.Mauipackage ships FCM and APNs providers an app can opt into, and IPushRegistrationService forwards the token to the backend.
MauiAccessibilityAnnouncer
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Accessibility·MMCA.Common.UI.Maui/Capabilities/Accessibility/MauiAccessibilityAnnouncer.cs:9· Level 1 · class (sealed)
- What it is: the MAUI-native adapter for IAccessibilityAnnouncer, pushing a spoken announcement to the platform screen reader (TalkBack, VoiceOver, Narrator) through
SemanticScreenReader.Default(MMCA.Common.UI.Maui/Capabilities/Accessibility/MauiAccessibilityAnnouncer.cs:5-8). - Depends on: IAccessibilityAnnouncer (the contract it implements); MAUI Essentials
SemanticScreenReader; BCLTaskandFeatureNotSupportedException. - Concept introduced: this is the first of the fifteen MAUI adapters in this unit, so the shared shape is worth stating once. Each class implements exactly one narrow capability interface (defined in
MMCA.Common.UI.Services.Capabilities, so shared components can depend on it from any head), wraps exactly one platform API, and is selected at DI composition time for the native head only. The adapter never branches on "which host am I": the container already answered that question.[Rubric §21, Accessibility]§21 assesses whether non-visual users receive the same information sighted users get. This adapter routes announcements through the OS assistive layer rather than a visual-only toast, and is a deliberate silent no-op when no screen-reader integration exists.[Rubric §2, Design Patterns]§2 assesses deliberate pattern use. Adapter plus Null Object is the pairing repeated across this entire capability family: a real platform adapter on the native head, an inert sibling everywhere else.
- Walkthrough: one member.
AnnounceAsync(string message, CancellationToken cancellationToken = default)(MauiAccessibilityAnnouncer.cs:12) calls the synchronousSemanticScreenReader.Default.Announce(message)(:16), catchesFeatureNotSupportedExceptionand drops the announcement when the platform has no screen-reader integration (:18-21), and returnsTask.CompletedTask(:23). The platform API is fire-and-forget and synchronous, so the async signature is satisfied with an already-completed task rather than an offloaded call. - Why it's built this way: swallowing the not-supported exception keeps call sites unconditional, so no component ever has to ask whether a screen reader is present before describing a change. Returning a completed task avoids a needless thread hop for what is a synchronous OS call.
- Where it's used: registered as a singleton
IAccessibilityAnnouncerbyAddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:56), which the head reaches throughUseMauiDeviceCapabilities()(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:30-43). Its siblings on the other heads are BrowserAccessibilityAnnouncer and the TryAdd default NullAccessibilityAnnouncer (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:49); consumers are live-update components announcing changes a sighted user perceives only visually.
MauiBatteryStatusService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.DeviceStatus·MMCA.Common.UI.Maui/Capabilities/DeviceStatus/MauiBatteryStatusService.cs:9· Level 1 · class (sealed partial,IDisposable)
- What it is: the MAUI adapter for IBatteryStatusService, reporting the OS energy-saver state and re-raising the platform's change event over
Battery.Default(MMCA.Common.UI.Maui/Capabilities/DeviceStatus/MauiBatteryStatusService.cs:5-7). - Depends on: IBatteryStatusService; MAUI Essentials
Battery,EnergySaverStatusandEnergySaverStatusChangedEventArgs; BCLIDisposableandEventHandler. - Concept introduced: the property-plus-change-event capability shape meets a subscription-lifetime concern. This is a singleton that hooks a static platform event in its constructor, so it must unhook in
Disposeor it pins itself for the life of the process.[Rubric §12, Performance & Scalability]and[Rubric §23, Front-End Performance]both assess whether the client adapts its workload to device constraints. Exposing the energy-saver flag lets live features throttle polling or decline to auto-join real-time channels when the OS says the device is conserving power.
- Walkthrough
- The constructor (
MauiBatteryStatusService.cs:12-13) subscribesOnEnergySaverStatusChangedtoBattery.Default.EnergySaverStatusChanged, so the instance observes transitions for its whole lifetime. EnergySaverChanged(MauiBatteryStatusService.cs:16): the contract's argument-free event, re-raised from the platform handler.IsEnergySaverOn(MauiBatteryStatusService.cs:19): readsBattery.Default.EnergySaverStatus == EnergySaverStatus.Onon every access, so subscribers re-read the live value instead of trusting stale event args.Dispose()(MauiBatteryStatusService.cs:22): unsubscribes from the platform event.OnEnergySaverStatusChanged(...)(MauiBatteryStatusService.cs:24-25): forwards the platform notification asEnergySaverChangedwithEventArgs.Empty.
- The constructor (
- Why it's built this way: subscribe-in-constructor and unsubscribe-in-
Disposeis the correct lifetime for a DI singleton holding a handler on a process-lifetime static. Re-reading the property rather than caching the args keeps one source of truth for the current state, which matters because a subscriber may handle the event after another transition has already occurred. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:47); its fallback sibling is NullBatteryStatusService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:52). Consumed by live and real-time components deciding whether to auto-join channels.
MauiBiometricAuthenticator
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Auth·MMCA.Common.UI.Maui/Capabilities/Auth/MauiBiometricAuthenticator.cs:13· Level 1 · class (sealed)
- What it is: the platform-direct adapter for IBiometricAuthenticator (ADR-042 Wave 4), driving the AndroidX
BiometricPrompton Android,LAContexton iOS and Mac Catalyst, and reporting unavailable on Windows (MMCA.Common.UI.Maui/Capabilities/Auth/MauiBiometricAuthenticator.cs:5-12). - Depends on: IBiometricAuthenticator; per-platform SDKs behind compilation symbols (AndroidX
BiometricManager,BiometricPromptandFragmentActivity;LocalAuthentication.LAContext); BCLTaskCompletionSource<T>and MAUIMainThread. - Concept introduced: fail-closed boolean auth gating implemented with
#ifpartitioning. Unlike every other adapter here, this class is not one implementation over a cross-platform Essentials API: the body is split three ways so each head compiles only its own SDK. The contract stays two methods regardless.[Rubric §11, Security]and[Rubric §26, Front-End Security]assess whether client-side auth degrades safely. Every negative outcome (cancel, lockout, error, an unsupported head) collapses tofalse, so callers fall back to credential login and never to a weaker path.[Rubric §22, Responsive/Cross-Browser]in its device-platform sense: the compile-time partition is what keeps a Windows build from referencing AndroidX types at all.
- Walkthrough
- Android (
MauiBiometricAuthenticator.cs:15-69):AllowedAuthenticatorscombinesBiometricWeak | DeviceCredential(:16-18), so a device PIN or pattern satisfies the prompt when no biometric is enrolled.IsAvailableAsync(:21) mapsBiometricManager.CanAuthenticateagainst those authenticators toBiometricSuccess(:23-25).AuthenticateAsync(:29) requires the current activity to be aFragmentActivityand returnsfalseotherwise (:31-34), builds aTaskCompletionSource<bool>withRunContinuationsAsynchronously(:36), and on the main thread (:38) resolves the main executor, resolvingfalseif it is null (:40-45), then shows aBiometricPrompttitled withreasonand restricted to the same authenticators (:47-52). Cancellation is registered to resolvefalse(:55) before awaiting the completion (:56). The nestedAuthenticationCallback(:59-69) setstrueon success (:62-63) andfalseon error (:65-66), and deliberately does not complete onOnAuthenticationFailed, because a single bad attempt leaves the prompt up (:68). - iOS and Mac Catalyst (
MauiBiometricAuthenticator.cs:70-92): both methods create ausingLAContextand work againstLAPolicy.DeviceOwnerAuthentication(Face ID or Touch ID with passcode fallback).IsAvailableAsyncreturnsCanEvaluatePolicy(:74-76);AuthenticateAsyncre-checks it, returnsfalsewhen it fails (:83-86), and otherwise returns thesuccesshalf of the policy evaluation tuple (:88-91). - Every other head, Windows included (
MauiBiometricAuthenticator.cs:93-101): both methods returnTask.FromResult(false), because the unpackaged WinUI head cannot presentUserConsentVerifier(:9-10).
- Android (
- Why it's built this way: folding every non-success into
falseforbids a partial-success path at the call site. Allowing device credentials alongside biometrics means a user with no enrolled biometric can still pass the app lock instead of being locked out of the feature. Not completing on a single failed attempt matches the platform prompt's own retry loop, which stays on screen until the user succeeds, cancels, or is locked out. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:61); the inert fallback is NullBiometricAuthenticator (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:53). Consumed by the stored-token auto-login app-lock gate, whose opt-in is persisted under a key from DevicePreferenceKeys. - Caveats / not-in-source: the token store and the auto-login flow live in the head apps and the Identity layer. This class only answers "is the enrolled device owner present right now".
MauiConnectivityStatusService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.DeviceStatus·MMCA.Common.UI.Maui/Capabilities/DeviceStatus/MauiConnectivityStatusService.cs:11· Level 1 · class (sealed partial,IDisposable)
- What it is: the MAUI adapter for IConnectivityStatusService, reporting network access and re-raising the change event over
Connectivity.Current(MMCA.Common.UI.Maui/Capabilities/DeviceStatus/MauiConnectivityStatusService.cs:5-10). - Depends on: IConnectivityStatusService; MAUI Essentials
Connectivity,NetworkAccessandConnectivityChangedEventArgs; BCLIDisposableandValueTask. - Concept introduced: offline-awareness at the UI edge, with the same singleton subscription lifetime MauiBatteryStatusService established.
[Rubric §29, Resilience & Business Continuity]§29 assesses graceful degradation when a dependency is unreachable. The offline banner and the request-skipping guards both read from this one flag, so degradation is decided in one place rather than per call site.
- Walkthrough
- The constructor (
MauiConnectivityStatusService.cs:14-15) subscribesOnPlatformConnectivityChangedtoConnectivity.Current.ConnectivityChanged. ConnectivityChanged(MauiConnectivityStatusService.cs:18): the contract event.IsOnline(MauiConnectivityStatusService.cs:21):Connectivity.Current.NetworkAccess == NetworkAccess.Internet. This is the load-bearing detail: captive-portal ("constrained") access counts as offline, because the API gateway is unreachable there, which is exactly what the offline banner should say (:6-9).InitializeAsync(CancellationToken = default)(MauiConnectivityStatusService.cs:24): returnsValueTask.CompletedTask. The native adapter subscribes in its constructor and needs no post-render listener setup, unlike the browser adapter that the contract's initialize method exists for.Dispose()(MauiConnectivityStatusService.cs:27): unsubscribes.OnPlatformConnectivityChanged(:29-30) forwards the event.
- The constructor (
- Why it's built this way: mapping only full
Internetaccess to online (rather than "some network exists") makes the banner honest about gateway reachability. The no-opInitializeAsynckeeps the always-ready native adapter allocation-free while still satisfying a contract shaped by the browser's needs. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:46); siblings are BrowserConnectivityStatusService and the shared default AlwaysOnlineConnectivityStatusService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:40). Consumed by the offline banner and request-skipping guards.
MauiFormFactor
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiFormFactor.cs:12· Level 1 · class (sealed)
- What it is: the MAUI implementation of IFormFactor, reporting the actual device idiom and platform through
DeviceInfo(MMCA.Common.UI.Maui/Capabilities/MauiFormFactor.cs:5-11). - Depends on: IFormFactor, which lives in
MMCA.Common.UI.Servicesrather than theCapabilitiesnamespace (MauiFormFactor.cs:1); MAUI EssentialsDeviceInfo. - Concept introduced: nothing new. This implements the older IFormFactor contract rather than a
Capabilitiesinterface, but it follows the same per-host adapter idea the rest of this unit uses.[Rubric §22, Responsive/Cross-Browser]§22 assesses whether the UI adapts across device classes. This class supplies the native head's real idiom, where the WebFormFactor and WasmFormFactor siblings can only report a web-derived answer.
- Walkthrough: two members, both expression-bodied.
GetFormFactor()(MauiFormFactor.cs:15):DeviceInfo.Idiom.ToString(), which yields Phone, Tablet or Desktop.GetPlatform()(MauiFormFactor.cs:18):DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString, for example Android, iOS, Windows or macOS with a version.
- Why it's built this way: the class was hoisted out of the app MAUI heads because it carries no app-specific state, so all native heads share one implementation instead of copy-pasting two one-line methods (
MauiFormFactor.cs:7-10). - Where it's used: registered as a singleton
IFormFactorbyAddMauiFormFactor()(MMCA.Common.UI.Maui/DependencyInjection.cs:143-144), which is deliberately kept separate fromAddMauiDeviceCapabilities()so heads that still register their own form factor keep last-registration-wins control (MMCA.Common.UI.Maui/DependencyInjection.cs:137-142). ADC's MAUI head calls it directly (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:169), and layout and responsive components consume it.
MauiHapticFeedbackService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.DeviceStatus·MMCA.Common.UI.Maui/Capabilities/DeviceStatus/MauiHapticFeedbackService.cs:11· Level 1 · class (sealed)
- What it is: the MAUI adapter for IHapticFeedbackService, firing tactile feedback over
HapticFeedback.DefaultandVibration.Default(MMCA.Common.UI.Maui/Capabilities/DeviceStatus/MauiHapticFeedbackService.cs:5-10). - Depends on: IHapticFeedbackService; MAUI Essentials
HapticFeedback,HapticFeedbackType,VibrationandPermissionException; BCLTimeSpanandOperatingSystem. - Concept introduced: decoration, not behavior. This is the one capability whose methods are synchronous and
void: nobody awaits a buzz, and nothing downstream depends on whether it happened.[Rubric §18, UI Architecture]§18 assesses whether presentation concerns stay off the correctness path. Every failure here is swallowed precisely so that a missing motor or a blocked permission can never change what the app does.
- Walkthrough
IsSupported(MauiHapticFeedbackService.cs:14):!OperatingSystem.IsWindows(), since Windows has no haptics.Click()andLongPress()(MauiHapticFeedbackService.cs:17,:20): route to the privatePerformwith the matchingHapticFeedbackType.Vibrate(TimeSpan duration)(MauiHapticFeedbackService.cs:23): callsVibration.Default.Vibrate(duration)(:27), catchingFeatureNotSupportedException(no motor or no platform support,:29-32) andPermissionException(the AndroidVIBRATEpermission missing from the host manifest,:33-36).Perform(HapticFeedbackType type)(MauiHapticFeedbackService.cs:39): callsHapticFeedback.Default.Perform(type)(:43), catching the same two exception types (:45-52).
- Why it's built this way: synchronous
voidmethods match the fire-and-forget nature of a UI micro-cue, and catching both the not-supported and the permission-missing cases means a head that forgot a manifest entry gets a silently plainer experience rather than an exception on a button click. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:50); the no-op fallback is NullHapticFeedbackService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:43). Consumed by interactive components such as bookmark toggles and poll votes.
NullPushRegistrationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Notifications/NullPushRegistrationService.cs:7· Level 1 · class (sealed)
- What it is: the no-op push registration for
IPushRegistrationService. Its summary explains why that is sufficient rather than a gap: web heads receive real-time notifications over the SignalR hub while the page is open and have no OS-level installation to manage (NullPushRegistrationService.cs:3-6). - Depends on:
IPushRegistrationService; BCLTask. - Concept: the two-half capability. Native push needs both a registration service and a token provider, and the framework defaults both to inert but replaces them at different times:
MMCA.Common.UI.Mauioverrides this registration service (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:67), while the token provider staysNullPushDeviceTokenProvideruntil the app opts in with real FCM or APNs credentials through a separate call (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/DependencyInjection.cs:118-123), so even a native head is registered-but-tokenless out of the box (DependencyInjection.cs:67-71). Reading those two defaults together is what makes the push pipeline's staged activation legible.[Rubric §22, Responsive / Cross-Browser]assesses per-head behavior. Web heads are not degraded here, they use a different delivery channel entirely (the SignalR hub), soIsSupportedfalse means "no OS registration to do", not "no notifications".
- Walkthrough
IsSupported(NullPushRegistrationService.cs:10): constantfalse; the notification-settings UI hides the native-push toggle.RegisterAsync(CancellationToken)(NullPushRegistrationService.cs:13):Task.FromResult(false), reporting that no registration was established.UnregisterAsync(CancellationToken)(NullPushRegistrationService.cs:16):Task.CompletedTask. Unregistering something that was never registered is a success, not a failure, which is why the two members differ in return shape.
- Why it's built this way: ADR-044, cited in the class summary (
NullPushRegistrationService.cs:4). Native push delivery is an additional channel layered on the existing hub, so the absence of a device registration must never be treated as the absence of notifications. - Where it's used:
TryAddSingletoninAddDeviceCapabilityDefaults()(DependencyInjection.cs:70), directly beside its token-provider counterpart (:55);MauiPushRegistrationServiceis the native override. - Caveats / not-in-source: unlike its siblings this class has no case in
CapabilityFallbackTests; its behavior is covered only through the DI defaults and the native override's own tests.
MauiExternalAuthBroker
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Auth·MMCA.Common.UI.Maui/Capabilities/Auth/MauiExternalAuthBroker.cs:19· Level 2 · class (sealed)
- What it is: the MAUI adapter for
IExternalAuthBroker(ADR-043), running an external OAuth sign-in through the platformWebAuthenticatorin the system browser and handing the captured completion code to the shared/auth/oauth-completepage. - Depends on:
IExternalAuthBroker;NavigationManager,IOptions<T>overApiSettings, andIConfiguration; MAUI EssentialsWebAuthenticator. This is the only type in the unit that composes over app configuration and navigation rather than a single platform static, which is also why it is the only one registered scoped rather than singleton (see below). - Concept: native OAuth callback capture, introduced at
IExternalAuthBroker.[Rubric §11, Security]assesses how credentials and identity flows are handled: identity providers reject embedded WebViews, so the flow runs in the system browser and only a single-use code (never a token) returns over the app's custom scheme.[Rubric §26, Front-End Security]assesses the browser-side half of the same concern: the shared completion page owns the code-to-token exchange and token storage, so the sensitive step exists in exactly one place across all heads (ADR-043). - Walkthrough
- Three readonly fields (
MauiExternalAuthBroker.cs:21-23): theNavigationManager, theIOptions<ApiSettings>, and the nullable callback scheme. - The constructor (
MauiExternalAuthBroker.cs:26) null-guardsconfiguration(:31), stores the first two dependencies, and reads the callback scheme fromconfiguration["OAuth:MobileRedirectScheme"](:35). IsAvailable(MauiExternalAuthBroker.cs:39): true only when the callback scheme is a non-blank string, so an unconfigured head keeps the web anchor flow.SignInAsync(string provider, CancellationToken = default)(MauiExternalAuthBroker.cs:42): guardsprovider(:44); returnsfalsewhen unavailable (:46-49) or when the configured API endpoint is missing (:51-55); builds{scheme}://oauth-completeas the callback and{apiBase}/auth/oauth/{provider}?returnUrl=...as the authorize URL, URL-escaping both the provider and the return URL (:57-59); callsWebAuthenticator.Default.AuthenticateAsyncwith those two URLs and the caller's token (:63-69); returnsfalseif no non-blankcodeproperty comes back (:71-76); otherwise navigates to/auth/oauth-complete?code=...and returnstrue(:80-81).TaskCanceledException(the user dismissed the browser) andFeatureNotSupportedExceptionboth returnfalse(:83-91).
- Three readonly fields (
- Why it's built this way: an unavailable default when the scheme is unset lets a single login page attempt native brokering and cleanly fall back to the web anchor flow. Delegating the exchange to the existing
/auth/oauth-completepage means the single-use-code contract, token storage, and auth-state refresh live in one place for every head (MauiExternalAuthBroker.cs:78-79, ADR-043). - Where it's used: registered scoped, not singleton, by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:76) because it navigates through the circuit'sNavigationManagerafter the system-browser round trip (MMCA.Common.UI.Maui/DependencyInjection.cs:73-75). The default it overrides is the singletonUnavailableExternalAuthBroker(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:55). Consumed by the login page's external-provider buttons. - Caveats / not-in-source: the code-to-token exchange, token storage, and auth-state refresh are not in this class; they live in the shared
/auth/oauth-completepage it navigates to. The class doc also records two out-of-code prerequisites: a server-side allow-list entry (OAuth:AllowedReturnUrlSchemes) and the platform callback registrations (MauiExternalAuthBroker.cs:14-17).
NullLocalNotificationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/NullLocalNotificationService.cs:4· Level 2 · class (sealed)
- What it is - the inert default for
ILocalNotificationService: on-device notification scheduling is unavailable. It denies permission and swallows every schedule and cancel call, and hosts readIsSupportedto hide reminder settings entirely (NullLocalNotificationService.cs:3). - Depends on - implements
ILocalNotificationService; acceptsLocalNotificationRequestandIReadOnlyCollection<int>ids. Null-object shape shared with the geo siblings above. - Concept introduced - none new, but note the two-signal contract this default has to satisfy cleanly. Scheduling notifications is native-only (there is no browser equivalent the framework wires), so the default has to make both the capability probe and the actions safe:
IsSupported == falsesteers the UI, and the action methods are no-ops so a caller that skips the probe still cannot crash. Same [Rubric §2 - Design Patterns] and [Rubric §22 - Responsive / Cross-Browser] framing asNullGeocodingService. - Walkthrough -
sealed class(NullLocalNotificationService.cs:4).IsSupported => false(NullLocalNotificationService.cs:7).RequestPermissionAsyncreturnsTask.FromResult(false)(NullLocalNotificationService.cs:10-11), reporting permission as not granted so a caller never proceeds to schedule.ScheduleAsync(NullLocalNotificationService.cs:14-15),CancelAsync(NullLocalNotificationService.cs:18-19), andCancelAllAsync(NullLocalNotificationService.cs:22) each returnTask.CompletedTask, doing nothing with their arguments;CancelAsyncin particular is already contractually allowed to ignore unknown ids (MMCA.Common.UI/Services/Capabilities/Notifications/ILocalNotificationService.cs:24-25), so ignoring all of them is a consistent extreme of the same rule. - Why it's built this way - ADR-042. The real contract already specifies that scheduling without permission is a no-op and that implementations never throw on denial (
MMCA.Common.UI/Services/Capabilities/Notifications/ILocalNotificationService.cs:6-8,21), so the null default is that rule taken to its limit: permission is never granted, therefore nothing is ever scheduled. Reminder features degrade to nothing on web without a single conditional in the feature code. - Where it's used - registered by
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:50); overridden byMauiLocalNotificationServiceon native heads (MMCA.Common.UI.Maui/DependencyInjection.cs:57). ADC'sSessionReminderCoordinatoris the shaped consumer: it takes the contract by constructor injection, re-exposesIsSupportedfor the UI to bind against, and short-circuits its schedule and cancel entry points when the capability reports unsupported (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Services/HappeningNow/SessionReminderCoordinator.cs:21,40,60,90). Web and Server heads keep this default.
NullPushDeviceTokenProvider
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Notifications·MMCA.Common.UI/Services/Capabilities/Notifications/NullPushDeviceTokenProvider.cs:9· Level 2 · class (sealed)
- What it is - a push token provider that never produces a token:
IPushDeviceTokenProviderimplemented to always returnnull. It is the default everywhere, native heads included, until a head opts in to a credentialed provider (NullPushDeviceTokenProvider.cs:3-8). - Depends on - implements
IPushDeviceTokenProvider; returnsPushDeviceToken?. Null-object shape shared with the siblings above, but note the different default reach (see below). - Concept introduced - inert-but-wired, distinct from unsupported. The earlier nulls in this unit mean "this head cannot do X." This one is subtler: it stays the resolved default even on a native head that can receive push, because push also needs external credentials (an FCM key, an APNs entitlement) that a plain build does not carry. Returning
nullleaves the entire registration pipeline present and correctly ordered but dormant, which the class doc calls out as "exactly the state a build without push credentials should be in" (NullPushDeviceTokenProvider.cs:6-7). Note there is noIsSupportedprobe on this contract at all (MMCA.Common.UI/Services/Capabilities/Notifications/IPushDeviceTokenProvider.cs:10-14): token presence is the signal, and the consuming registration service treats anulltoken as "nothing to register" rather than as a failure. Same [Rubric §2 - Design Patterns] framing asNullGeocodingService.- [Rubric §32 - Dependency & Supply-Chain] §32 assesses how external dependencies enter a build. Keeping the token source behind a swappable contract means push credentials and their SDKs are a per-head opt-in rather than something every consumer of the UI package inherits.
- [Rubric §29 - Resilience & Business Continuity] §29 assesses graceful behavior at edge states. A tokenless build is a first-class, non-throwing state here, not an error path.
- Walkthrough -
sealed class(NullPushDeviceTokenProvider.cs:9). A single method,GetTokenAsync(NullPushDeviceTokenProvider.cs:12-13), returnsTask.FromResult<PushDeviceToken?>(null). There is deliberately noIsSupportedmember, because the contract declares none. - Why it's built this way - ADR-044 (native push delivery), cited in the class summary (
NullPushDeviceTokenProvider.cs:4). The registration path is split into two independently overridable pieces on purpose:MMCA.Common.UI.Mauioverrides the registration service as part of its standard capability block, while the token provider is a separate opt-in, so a MAUI head that never calls the opt-in stays registered-but-tokenless. The shared registration comment states that pairing directly (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:58-62). - Where it's used -
TryAdd-registered as a singleton byAddDeviceCapabilityDefaultsalongsideNullPushRegistrationService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:61-62). It is consumed byMauiPushRegistrationService, which asks for a token first and returnsfalsewithout any HTTP call when it getsnull(MMCA.Common.UI.Maui/Capabilities/Notifications/MauiPushRegistrationService.cs:17,32-36). A native head displaces this default by callingAddMauiPushDeviceTokenProvider()(MMCA.Common.UI.Maui/DependencyInjection.cs:118-126), which registersFcmPushDeviceTokenProvideron the Android TFM (:105) andApnsPushDeviceTokenProvideron iOS/MacCatalyst (:107) and registers nothing on the windows TFM, leaving this default in place there (MMCA.Common.UI.Maui/DependencyInjection.cs:104-107). ADC's MAUI head makes that call (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:122); Store's does not, so Store keeps this default on every TFM. - Caveats / not-in-source - opting in is not the same as activating. Both credentialed providers are configuration-gated (
Push:Fcmcredentials,Push:Apns:Enabled) and depend on app-side platform wiring the framework cannot supply, so a head that callsAddMauiPushDeviceTokenProvider()without that configuration behaves the same as this null default (MMCA.Common.UI.Maui/DependencyInjection.cs:110-115). Whether any given build actually carries those credentials is deployment configuration and is not determinable from source.
MauiCultureStore
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Globalization·MMCA.Common.UI.Maui/Globalization/MauiCultureStore.cs:19· Level 1 · class (internal static)
- What it is: the one place a MAUI Blazor Hybrid head stores, resolves, and activates the UI culture (ADR-027). It is three static methods over
Preferences.Defaultand theCultureInfothread defaults, shared byMauiCultureApplier(the user switching language) andMauiCultureInitializer(the startup restore). - Depends on:
SupportedCultures(MMCA.Common.Shared.Globalization,MMCA.Common.UI.Maui/Globalization/MauiCultureStore.cs:2); MAUI EssentialsPreferences.Default; BCLSystem.Globalization.CultureInfo(:1). - Concept introduced, culture as process state instead of request state, and the
AsyncLocaltrap that comes with it. On a web head the culture is per request: a cookie is written,CookieRequestCultureProviderreads it, and ASP.NET sets the culture inside that request's own execution context. A hybrid head has no request pipeline at all, so there is exactly one process and one ambient culture (:6-11). The type doc also records why this class bypassesIDevicePreferenceseven though that contract exists in the same package (:12-17):IDevicePreferencesis async-only and the startup restore runs from the synchronousIMauiInitializeService.Initializehook, so routing one side through the async store would give a single value two storage paths.[Rubric §27, i18n]assesses whether the app can genuinely operate in more than one language, including persistence of the user's choice; this is where the hybrid head's choice survives a restart.[Rubric §19, State Management]assesses whether state has one owner and one lifetime; the whole point of this class is that the culture has exactly one. - Walkthrough
PreferenceKey = "mmca.culture"(MMCA.Common.UI.Maui/Globalization/MauiCultureStore.cs:25): deliberately outside themmca.devicePrefs.prefix thatMauiDevicePreferencesuses, because this value never goes through that store. The doc warns that changing the key silently resets every installed app to the device locale (:21-24).Save(string culture)(:29): a one-linePreferences.Default.Set, best-effort like the rest of the layer.Resolve()(:37-44): reproduces the web heads' precedence order without a request. It reads the stored value (:39), returns it whenSupportedCultures.IsSupported(stored)(the cookie's analogue,:41-42), and otherwise falls back toSupportedCultures.ResolveClosest(CultureInfo.CurrentUICulture.Name), which is theAccept-Languageanalogue: anes-MXdevice lands ones(:43). A non-matching device locale falls through to the framework default insideResolveClosest.ApplyToProcess(string culture)(:68-74): constructs theCultureInfo(:70) and assigns onlyCultureInfo.DefaultThreadCurrentCultureandDefaultThreadCurrentUICulture(:72-73).
- Why it's built this way: the
ApplyToProcessdoc (:46-66) carries the single most load-bearing explanation in this group. AssigningCultureInfo.CurrentCulture/CurrentUICulturewrites to anAsyncLocal, so the value flows with theExecutionContextand is restored every time that context is re-entered, outranking the thread defaults. The startup restore runs before any window exists, so the context it would write to is the ancestor of every later dispatch including the Blazor renderer's; a later switch could then set the defaults toesand still re-render forever in the launch language. Setting only the thread defaults means no thread ever materializes a culture of its own, so one switch takes effect everywhere at once. A web head never meets this because request localization sets the culture inside each request's own context. - Where it's used: called by
MauiCultureApplier(SavethenApplyToProcess) and byMauiCultureInitializer(ApplyToProcess(Resolve())). It isinternal(:19), so nothing outside theMMCA.Common.UI.Mauipackage can reach it.
MauiPublicLinkBuilder
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Services·MMCA.Common.UI.Maui/Services/MauiPublicLinkBuilder.cs:14· Level 1 · class (sealed)
- What it is: the native-head implementation of
IPublicLinkBuilder. It turns a relative path into an absolute URL rooted at the public web app, so a link the user shares or copies from the device points at the public site rather than the WebView's internal origin. - Depends on:
IPublicLinkBuilder(MMCA.Common.UI.Services,MMCA.Common.UI.Maui/Services/MauiPublicLinkBuilder.cs:2,:14);Microsoft.Extensions.Configuration.IConfiguration(:1,:24); BCLUri. - Concept introduced, per-head override of a shared UI service. On a browser head the default
NavigationPublicLinkBuildercan resolve against the current origin, because that origin IS the public site. A MAUIBlazorWebViewreports an internal shell address that is meaningless once pasted into a message, so this head substitutes a base URL pinned in the head's embeddedappsettingsunderPublicSite:BaseUrl(:6-13). The override works only because it is registered with a plainAddafterAddUISharedand after any module registration that supplies a builder of its own, so last-registration-wins (:9-12, and the DI doc atMMCA.Common.UI.Maui/DependencyInjection.cs:128-133).[Rubric §25, Navigation & IA]assesses whether links resolve to real destinations regardless of where they were produced; that is exactly what this builder guarantees.[Rubric §26, Front-End Security]applies because the shared URL is bound to one configured host instead of whatever origin the WebView happens to report. - Walkthrough
BaseUrlConfigKey = "PublicSite:BaseUrl"(MMCA.Common.UI.Maui/Services/MauiPublicLinkBuilder.cs:17): apublic const, so a head can reference the same key when it validates its own configuration.- The constructor (
:24-33): null-guards the configuration (:26), reads the key (:28), and throwsInvalidOperationExceptionwhen it is missing or blank (:29-31), a fail-fast that stops a misconfigured build from silently emitting broken share links. Otherwise it parses the value as an absoluteUriinto the readonly_baseUrlfield (:19,:32). BuildAbsolute(string relativePath)(:36-41): guards withArgumentException.ThrowIfNullOrWhiteSpace(relativePath)(:38), then combines the path onto the base via theUri(baseUri, relative)constructor (:40).
- Why it's built this way:
UriKind.Absoluteat construction plus the blank check means a badPublicSite:BaseUrlfails once, at container build, rather than producing a plausible but wrong link on every share. The class doc notes the value is pinned by the same mechanism as the head's gateway endpoint (:8-10), so one embedded configuration file defines both where the app talks and what it links to. - Where it's used: registered scoped as
IPublicLinkBuilderbyAddCommonMauiPublicLinkBuilder()(MMCA.Common.UI.Maui/DependencyInjection.cs:134-135), which replaces theTryAddScopeddefaultNavigationPublicLinkBuilderinstalled byAddUIShared(MMCA.Common.UI/DependencyInjection.cs:131). Consumed by the share, copy-link and QR affordances that pair withIShareServiceandIClipboardService.
MauiSecureTokenStore
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Services·MMCA.Common.UI.Maui/Services/MauiSecureTokenStore.cs:22· Level 1 · class (sealed)
- What it is: the raw half of the MAUI token pipeline, an
ISecureTokenStorethat keeps the access and refresh tokens in MAUISecureStorage(Android Keystore, iOS Keychain, Windows DPAPI) with every single call wrapped so an OS-invalidated entry can never crash the app. It has no freshness semantics: it reads back exactly what was written. - Depends on:
ISecureTokenStore(MMCA.Common.UI.Services.Auth,MMCA.Common.UI.Maui/Services/MauiSecureTokenStore.cs:1,:22); MAUI EssentialsSecureStorage.Default; BCLTask. It depends on nothing else, which is the point of the split described below. - Concept introduced, fail-to-signed-out, the recovery posture for platform secure storage. Keystore and Keychain entries are invalidated by the OS on its own schedule: an Android backup restored onto a new device, a security patch that rotates the master key, a biometric enrolment change. The raw APIs then throw a platform-specific exception rather than returning nothing, and an unhandled throw in a token read happens on launch, which bricks the app until it is reinstalled (
:8-15). The class therefore turns every failure into the one state that is always recoverable by the user: signed out. The second idea here is the acyclic split the interface exists for:ITokenStorageServicedepends onITokenRefresher, which depends on this raw store, so the graph runs storage to refresher to raw store with no loop and no runtime re-entrancy (MMCA.Common.UI/Services/Auth/Tokens/ISecureTokenStore.cs:4-9).[Rubric §11, Security]assesses whether credentials are protected at rest with platform-appropriate mechanisms; using the enclave-backed store rather than plain preferences is that.[Rubric §26, Front-End Security]assesses client-side credential handling specifically; the invariant that a failed write leaves no tokens (never a stale pair) is the security-relevant half.[Rubric §29, Resilience & Business Continuity]assesses graceful degradation; one clean re-login is the designed worst case. - Walkthrough
AccessTokenKey/RefreshTokenKey(MMCA.Common.UI.Maui/Services/MauiSecureTokenStore.cs:24-25): the twoauth_*entry names.GetAccessTokenAsync()/GetRefreshTokenAsync()(:28,:31): thin forwards to the privateGetAsync.SetTokensAsync(string accessToken, string refreshToken)(:34-53): writes the refresh token first, then the access token, with both writes inside onetry(:40-44). Any failure drops both entries viaTryRemoveand rethrows (:46-52). The inline comment (:36-39) records the bug this shape fixes: a failing refresh write used to escape before the guard was entered, leaving the OLD pair in place, so the app held a stale access token it believed was current until a manual sign-out.ClearTokensAsync()(:56-63): twoTryRemovecalls and a completed task, never throwing. The comment states the reasoning plainly (:58-59): an entry that cannot be deleted is one the OS already invalidated, which is the outcome the caller asked for.GetAsync(string key)(:69-85): returnsSecureStorage.Default.GetAsync(key)(:73), and on any exception removes the entry and returnsnull(:82-83), so the next write starts from a clean key. The catch is a barecatchunder a scoped#pragma warning disable CA1031(:75,:77) because the thrown type differs per OS and none of them are recoverable here. The comment also records that nothing in the MAUI head takes anILogger, so the swallow is documented in code rather than reported (:79-81).SetAsync(string key, string value)(:91-104): writes (:95), and on failure removes the key and retries the write once (:101-102). A second failure propagates, because a caller must never believe a token was persisted when it was not (:88-89).TryRemove(string key)(:107-120): best-effort delete; a delete that itself throws is already the goal, and the nextSetAsyncoverwrites the entry anyway (:117-118).
- Why it's built this way: the asymmetry between reads and writes is the design. A failed read is survivable by returning
null(whichISecureTokenStorealready documents as "no token stored",MMCA.Common.UI/Services/Auth/Tokens/ISecureTokenStore.cs:18-22), so it is swallowed. A failed write is not survivable silently, so it propagates, but only after storage has been forced into the clean signed-out state. Remove-then-retry on write is the concrete remedy for the common cause: an entry whose encryption key the OS rotated cannot be overwritten in place but can be recreated. - Where it's used: registered scoped as
ISecureTokenStorebyAddCommonMauiTokenStorage()(MMCA.Common.UI.Maui/DependencyInjection.cs:99), alongside the layer above it,MauiTokenStorageService(:84). The browser hosts have no equivalent: they hold the access token in memory and keep the refresh token in an HttpOnly cookie, so they implement no raw store at all (MMCA.Common.UI/Services/Auth/Tokens/ISecureTokenStore.cs:10-14).
MauiTokenStorageService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Services·MMCA.Common.UI.Maui/Services/MauiTokenStorageService.cs:19· Level 1 · class (sealed)
- What it is: the freshness-checking
ITokenStorageServicethat sits on top ofMauiSecureTokenStore. Raw persistence is delegated; this type adds what a long-lived mobile session needs, namely a proactive refresh when the stored access token is at or near expiry, and a single-flight guarantee so concurrent callers share one refresh. - Depends on:
ITokenStorageService(the contract,MMCA.Common.UI.Maui/Services/MauiTokenStorageService.cs:21),ISecureTokenStoreandITokenRefresher(primary-constructor parameters,:19-21), andJwtTokenInfo(:33); BCLSystem.Threading.Lock(:25). - Concept introduced, single-flight acquisition. A mobile head has several independent consumers of the access token: the delegating handler on every API call, the auth-state provider, and the SignalR connection (
:9-12). If two of them find the token stale at the same moment and each starts its own refresh, the second refresh rotates the refresh token again and invalidates the pair the first caller is still holding, so a purely additive fix (_hydrateInFlight ??= HydrateAsync()with no lock) is not enough. The pattern here is: take a short lock only long enough to publish or read the shared task, await it outside the lock, then clear the slot only if it is still yours.[Rubric §12, Performance & Scalability]assesses whether the system avoids redundant work under concurrency; collapsing N refreshes into one is that.[Rubric §11, Security]applies because token rotation makes the redundant refresh actively harmful, not merely wasteful.[Rubric §1, SOLID]applies to the split itself: freshness policy and raw persistence are two responsibilities and now live in two types, which is also what keeps the DI graph acyclic. - Walkthrough
ExpirySkew = TimeSpan.FromSeconds(30)(MMCA.Common.UI.Maui/Services/MauiTokenStorageService.cs:23): the head start. A token that expires inside the next 30 seconds is treated as already stale, so a refresh happens before a request can fail._hydrateSync(:25), aSystem.Threading.Lock, and_hydrateInFlight(:27), the nullable sharedTask<string?>.GetAccessTokenAsync()(:30-66): reads the stored token from the raw store (:32) and returns it verbatim whenJwtTokenInfo.IsFresh(stored, ExpirySkew)(:33-36), the fast path that touches no lock. Otherwise it enters the lock, assigns_hydrateInFlight ??= HydrateAsync(), and copies the task to a local (:43-48); the comment at:38-42records both why the lock is needed and why holding it is cheap (HydrateAsyncreaches its first await immediately, so nothing slow runs under it). It awaits the shared task (:52), and in afinallyre-takes the lock and clears the field only when it still references the same task (:58-64), the guard the comment at:56-57explains: an unguarded clear can drop a newer hydrate started after this one completed, splitting the next set of callers again.GetRefreshTokenAsync()(:69),SetTokensAsync(...)(:72-73),ClearTokensAsync()(:76): straight pass-throughs to the raw store. Only the access-token read has freshness semantics.HydrateAsync()(:78-79): one awaitedtokenRefresher.AcquireAccessTokenAsync().
- Why it's built this way: returning a stale bearer verbatim is not a silent problem; it produces a 401 that the user experiences as a random sign-out mid-session (
:9-12). The stored token can be hours or days old because it survives app restarts, so unlike a web head there is no request boundary at which the token is naturally re-fetched. The class doc namesWasmTokenStorageServiceas the type this mirrors (:7-8), so the same freshness contract holds on every head even though the storage underneath differs. - Where it's used: registered scoped as
ITokenStorageServicebyAddCommonMauiTokenStorage()(MMCA.Common.UI.Maui/DependencyInjection.cs:100), which wiresMauiSecureTokenStorein the same call (:83). The browser-host siblings areWasmTokenStorageServiceandServerTokenStorageService(:14-16). - Caveats / not-in-source: the single-flight guarantee is per DI scope, because the registration is scoped. Whether a MAUI head ever creates more than one scope concurrently is not determinable from this file.
MauiClipboardService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Interop·MMCA.Common.UI.Maui/Capabilities/Interop/MauiClipboardService.cs:6· Level 1 · class (sealed)
- What it is: the MAUI adapter for IClipboardService, writing text to the system clipboard over
Clipboard.Default(MMCA.Common.UI.Maui/Capabilities/Interop/MauiClipboardService.cs:5). - Depends on: IClipboardService; MAUI Essentials
Clipboard; BCLFeatureNotSupportedException. - Concept introduced: best-effort capability with a reported outcome. Several adapters in this family swallow failure silently; this one converts it into a
boolinstead.[Rubric §18, UI Architecture]§18 assesses whether the presentation layer gives components what they need to render honestly. Theboolresult is what lets a caller show a "copied" confirmation only when the write actually landed, rather than lying on a head with no clipboard.
- Walkthrough: one member.
SetTextAsync(string text, CancellationToken cancellationToken = default)(MauiClipboardService.cs:9) awaitsClipboard.Default.SetTextAsync(text)and returnstrue(:13-14), or returnsfalseonFeatureNotSupportedException(:16-19). - Why it's built this way: reporting success rather than returning
voidmakes this adapter the copy-link fallback signal for IShareService callers on heads where a native share sheet is unavailable. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:49), next to BrowserClipboardService and the TryAdd default NullClipboardService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:42) on the other heads. Consumed by the copy-link fallback path of IShareService.
MauiDevicePreferences
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.DeviceStorage·MMCA.Common.UI.Maui/Capabilities/DeviceStorage/MauiDevicePreferences.cs:12· Level 1 · class (sealed)
- What it is: the MAUI adapter for IDevicePreferences, a typed key/value store for per-device settings backed by
Preferences.Default(MMCA.Common.UI.Maui/Capabilities/DeviceStorage/MauiDevicePreferences.cs:6-11). - Depends on: IDevicePreferences; MAUI Essentials
Preferences; BCLSystem.Text.Json. - Concept introduced: device-scoped client state, with keys drawn from DevicePreferenceKeys so the same setting resolves identically on every head.
[Rubric §19, State Management]§19 assesses whether each piece of state has a clear owner and lifetime. These values describe this device (haptics on, app lock enabled) and deliberately never roam with the account, which is why they live here rather than in a server-side profile.[Rubric §26, Front-End Security]§26 assesses what the client persists. The XML doc is explicit that secrets never belong here: those go toSecureStorage(MauiDevicePreferences.cs:9-10).
- Walkthrough
KeyPrefix = "mmca.devicePrefs."(MauiDevicePreferences.cs:14): every key is namespaced under one prefix, mirroring the browser adapter so key/value semantics hold on every head.IsPersistent(MauiDevicePreferences.cs:17):true, because these values survive an app restart.GetAsync<T>(string key, T fallback, CancellationToken = default)(MauiDevicePreferences.cs:20): guards the key withArgumentException.ThrowIfNullOrWhiteSpace(:22), reads the prefixed raw string and returnsfallbackwhen absent (:24-28), then JSON-deserializes, returningfallbackon a null result (:33) or onJsonException(:35-38).SetAsync<T>(string key, T value, CancellationToken = default)(MauiDevicePreferences.cs:42): guards the key, then writesJsonSerializer.Serialize(value)under the prefixed key (:46).RemoveAsync(string key, CancellationToken = default)(MauiDevicePreferences.cs:51): guards the key and removes the prefixed entry (:55).
- Why it's built this way: JSON-encoding every value under one prefix gives the same typed store across MAUI and browser heads with no per-type platform code, and a bad key is caught immediately while a corrupt value degrades to the caller's
fallbackrather than throwing into a render path. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:59); siblings are BrowserDevicePreferences and the volatile Blazor Server default InMemoryDevicePreferences, which is the one capability default registered scoped rather than singleton so a circuit's preferences never leak across users (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:72-74). Read and written by device-settings screens and the app-lock gate.
MauiExternalLinkService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Interop·MMCA.Common.UI.Maui/Capabilities/Interop/MauiExternalLinkService.cs:10· Level 1 · class (sealed)
- What it is: the MAUI adapter for IExternalLinkService, opening external URLs in the system browser or the OS handler, because
target="_blank"dead-ends inside a BlazorWebView (MMCA.Common.UI.Maui/Capabilities/Interop/MauiExternalLinkService.cs:5-9). - Depends on: IExternalLinkService; MAUI Essentials
Browser,BrowserLaunchModeandLauncher; BCLUriandFeatureNotSupportedException. - Concept introduced: the WebView dead-link workaround. A hybrid head renders web markup inside a native shell that has no notion of a second browser tab, so an anchor that would open a new tab on the web simply does nothing. The capability lets shared components emit one markup shape and have the host decide whether to intercept.
[Rubric §25, Navigation & IA]§25 assesses whether the user always ends up somewhere coherent. Without interception, an external link inside the WebView is a dead click; with it, the link leaves the app the way the platform expects.[Rubric §18, UI Architecture]the branch lives once, in the adapter, not in every component that renders a link.
- Walkthrough
InterceptsLinks(MauiExternalLinkService.cs:13):true, telling shared components to route throughOpenAsyncrather than render a raw anchor.OpenAsync(Uri uri, CancellationToken = default)(MauiExternalLinkService.cs:16): null-guardsuri(:18); forhttpandhttps(compared withUri.UriSchemeHttp/HttpsunderOrdinalIgnoreCase) it usesBrowser.Default.OpenAsync(uri, BrowserLaunchMode.SystemPreferred)and returns (:22-27); everything else (mailto:,tel:,sms:) goes toLauncher.Default.TryOpenAsync(:31), becauseBrowser.Defaultonly accepts http(s) (:29-30).FeatureNotSupportedExceptionis swallowed: the link is a convenience, not a workflow (:33-36).
- Why it's built this way: splitting web schemes (system browser) from contact schemes (OS launcher) is what makes
mailto:andtel:links work from inside the WebView, where a plain anchor would silently do nothing and the browser API would reject the scheme outright. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:54); siblings are BrowserExternalLinkService and the TryAdd default NullExternalLinkService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:47). Consumed by the shared external-link component.
MauiLocalCacheStore
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.DeviceStorage·MMCA.Common.UI.Maui/Capabilities/DeviceStorage/MauiLocalCacheStore.cs:11· Level 1 · class (sealed)
- What it is: the MAUI adapter for ILocalCacheStore, storing JSON documents as files in an
mmca-cachefolder under the app data directory (MMCA.Common.UI.Maui/Capabilities/DeviceStorage/MauiLocalCacheStore.cs:6-10). - Depends on: ILocalCacheStore; BCL
System.IO(File,Path,Directory), MAUIFileSystem, andSystem.Text.Json. - Concept introduced: last-known-good UI state for offline rendering. Unlike IDevicePreferences, which holds settings the user chose, this holds server data the app may need to redraw without a network.
[Rubric §29, Resilience & Business Continuity]§29 assesses degradation under dependency failure. Paired with IConnectivityStatusService, the on-device cache lets shared components render a snapshot when the API is unreachable instead of an empty screen.
- Walkthrough
IsAvailable(MauiLocalCacheStore.cs:14):true, because a native head always has a writable data directory.SetAsync<T>(string key, T value, CancellationToken = default)(MauiLocalCacheStore.cs:17): guards the key (:19), resolves the path creating the directory (:23), serializes (:24), andFile.WriteAllTextAsyncs (:25), catchingIOExceptionandUnauthorizedAccessExceptionbecause a failed write only means a colder next launch (:27-34).GetAsync<T>(string key, CancellationToken = default)(MauiLocalCacheStore.cs:38): guards the key (:40), returnsdefaultwhen the file does not exist (:45-48), otherwise reads and deserializes (:50-51), collapsingIOException,UnauthorizedAccessExceptionandJsonExceptiontodefault(:53-64).RemoveAsync(string key, CancellationToken = default)(MauiLocalCacheStore.cs:68): deletes the file (:74), swallowing the same IO failures (:76-83), and returns a completed task (:85).GetPath(string key, bool ensureDirectory)(MauiLocalCacheStore.cs:88): buildsmmca-cacheunderFileSystem.AppDataDirectory(:90), optionally creates it (:91-94), and maps the key to a file name through a conservative character filter that keeps ASCII letters, digits,-and.and replaces everything else with_, then appends.json(:96-97). The class doc notes keys are code-controlled, not user input (:8-9).
- Why it's built this way: file-per-key JSON keeps the store dependency-free (no embedded database to ship or migrate), and best-effort IO with
defaultreturns means a cache miss or a corrupt file degrades to a live fetch rather than surfacing an error to the user. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:60); siblings are BrowserLocalCacheStore and the permanently unavailable NullLocalCacheStore (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:56). Consumed by offline-capable list and schedule components. - Caveats / not-in-source: the filter maps distinct keys onto the same file name when they differ only in filtered characters. Nothing in this class detects that collision; the doc comment's "keys are code-controlled" is the mitigation.
MauiMapNavigationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Geo·MMCA.Common.UI.Maui/Capabilities/Geo/MauiMapNavigationService.cs:11· Level 1 · class (sealed)
- What it is: the MAUI adapter for IMapNavigationService, launching the platform maps app for a street address through
LauncherURIs (MMCA.Common.UI.Maui/Capabilities/Geo/MauiMapNavigationService.cs:5-10). - Depends on: IMapNavigationService; MAUI Essentials
Launcher; BCLUriandOperatingSystem. - Concept introduced: address-only navigation. The capability takes a postal address, not coordinates, because the domain model carries no geo-coordinates and the OS maps app can geocode far better than the app could.
[Rubric §18, UI Architecture]and[Rubric §30, Compliance/Privacy]: handing the address to the OS means the app never requests a location permission and never sees where the user is, which is the cheapest possible privacy posture for a "get directions" button.
- Walkthrough
OpenAddressAsync(string address, string? label, CancellationToken = default)(MauiMapNavigationService.cs:14): guards the address (:16), URL-escapes it withUri.EscapeDataString(:18), builds the platform URI (:19), and returns the result ofLauncher.Default.TryOpenAsync(uri)(:23), orfalseonFeatureNotSupportedException(:25-28). Thelabelparameter is accepted by the contract but unused by this adapter.BuildPlatformUri(string escapedQuery)(MauiMapNavigationService.cs:31):geo:0,0?q=...on Android (:36-39),https://maps.apple.com/?q=...on iOS and Mac Catalyst (:41-44), andbingmaps:?q=...everywhere else (:46). The method brackets its body with a SonarAnalyzerS1075suppression (:35,:47) whose comment records the reasoning: these launcher URIs are the per-platform maps integration point, fixed by the OS rather than environment-dependent, which is what S1075 actually targets (:33-34).
- Why it's built this way: routing through the OS launcher instead of an in-app map control needs no location permission, no map SDK and no geocoding round-trip. Hard-coding the scheme per platform is correct here because these are OS contracts, and the class doc notes Android hosts must declare a
geointent in the manifest<queries>block for the launcher to resolve it (MauiMapNavigationService.cs:9). - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:51); siblings are BrowserMapNavigationService and NullMapNavigationService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:44). Consumed by venue and location components.
MauiScreenshotService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Interop·MMCA.Common.UI.Maui/Capabilities/Interop/MauiScreenshotService.cs:10· Level 1 · class (sealed)
- What it is: the MAUI adapter for IScreenshotService, capturing the current screen to a temporary PNG through
Screenshot.Default(MMCA.Common.UI.Maui/Capabilities/Interop/MauiScreenshotService.cs:5-9). - Depends on: IScreenshotService; MAUI Essentials
Screenshot,ScreenshotFormatandFileSystem; BCLSystem.IOandGuid. - Concept introduced: permissionless temp-file capture. Where the file lands is the whole design.
[Rubric §30, Compliance/Privacy]and[Rubric §26, Front-End Security]: captures go to the platform cache directory and never the photo library, so no storage permission is prompted or held, and the OS is free to reclaim the files when it needs space.
- Walkthrough
IsSupported(MauiScreenshotService.cs:13):Screenshot.Default.IsCaptureSupported.CaptureToFileAsync(CancellationToken = default)(MauiScreenshotService.cs:16): re-checks capture support and returnsnullearly when it is unavailable (:18-21); otherwise captures (:25), builds a path ofmmca-screenshot-{guid:N}.pngunderFileSystem.CacheDirectory(:26), opens the capture as a PNG stream and copies it to a created file through nestedawait usingblocks that dispose both streams deterministically (:28-36), and returns the path (:38). BothFeatureNotSupportedExceptionandIOExceptionreturnnull(:40-47).
- Why it's built this way: writing to the cache directory keeps the feature permission-free, and the nullable path return lets the share flow abort quietly when capture is unsupported or the write fails, rather than forcing every caller into a try/catch.
- Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:58); the unsupported fallback is NullScreenshotService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:51). Its output path is handed straight to IShareService for image sharing.
MauiShareService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Interop·MMCA.Common.UI.Maui/Capabilities/Interop/MauiShareService.cs:6· Level 1 · class (sealed)
- What it is: the MAUI adapter for IShareService, opening the native share sheet for a link or a local file over
Share.Default(MMCA.Common.UI.Maui/Capabilities/Interop/MauiShareService.cs:5). - Depends on: IShareService; MAUI Essentials
Share,ShareTextRequest,ShareFileRequestandShareFile; BCLUri,FeatureNotSupportedExceptionandIOException. - Concept introduced: share with a copy-link fallback. The interface was shaped around the fact that not every head has a share sheet, so both methods answer
boolrather thanvoid.[Rubric §18, UI Architecture]: those boolean returns are exactly what make an IClipboardService copy-link a viable second choice, keeping a Share button useful on every head.
- Walkthrough
ShareLinkAsync(string title, Uri uri, CancellationToken = default)(MauiShareService.cs:9): null-guardsuri(:11), requests aShareTextRequestcarryingTitleand the URI string (:15-19), and returnstrue(:20), orfalseonFeatureNotSupportedException(:22-25).ShareFileAsync(string title, string filePath, string contentType, CancellationToken = default)(MauiShareService.cs:29): guardsfilePath(:31), requests aShareFileRequestwrappingnew ShareFile(filePath, contentType)(:35-39), and returnstrue(:40), orfalseonFeatureNotSupportedExceptionorIOException(:42-49).
- Why it's built this way: presenting the OS share sheet reuses the platform's own target picker, so the app never has to enumerate or authenticate against individual destinations. Reporting failure as
falselets the calling component degrade to copy-link instead of showing an error. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:48); siblings are BrowserShareService and NullShareService (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:41). Consumes IScreenshotService output for image sharing and falls back to IClipboardService.
MauiCultureApplier
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Globalization·MMCA.Common.UI.Maui/Globalization/MauiCultureApplier.cs:22· Level 2 · class (sealed)
- What it is: the hybrid-head implementation of
ICultureApplier(ADR-027). When the user picks a language, it persists and activates the choice in process and force-reloads theBlazorWebViewso every component re-renders under the new culture. - Depends on:
ICultureApplier(the contract,MMCA.Common.UI.Maui/Globalization/MauiCultureApplier.cs:3,:22),MauiCultureStore(persistence plus activation,:41-42),SupportedCultures(the allowlist,:2,:32), andNavigationManager(primary-constructor parameter,:1,:22). - Concept introduced, force-load as a re-render mechanism. The class doc (
:13-19) explains the reasoning: resource strings are resolved fromCultureInfo.CurrentUICultureat render time, and Blazor exposes no API to re-render an entire component tree in place, so a full reload is the only way to make the switch visible everywhere at once. Inside aBlazorWebViewthis is cheap in a way it is not on the web: the force-load re-boots the Blazor app inside the WebView while the .NET process (and therefore the culture just set) stays alive. It also introduces the head-specific replacement of a shared default: the web defaultEndpointCultureAppliernavigates to the server/culture/setendpoint, which on a hybrid head is resolved by the BlazorRouter, matches no page, and renders the not-found page (:8-12).[Rubric §27, i18n]assesses whether language is a first-class, switchable concern; this is the switch.[Rubric §18, UI Architecture]applies because one language-switcher component works on every head purely by resolving a differentICultureApplier. - Walkthrough:
ApplyAsync(string culture, string returnPath, CancellationToken = default)(MMCA.Common.UI.Maui/Globalization/MauiCultureApplier.cs:25-48) guards the culture string (:27), then checksSupportedCultures.IsSupported(culture)and returns a completed task unchanged when it fails (:32-35), which is exact parity with the web endpoint's allowlist behavior. The comment at:29-31records that the pseudo locale is unreachable here: the switcher only offers it whenIHostEnvironmentreports Development, and a MAUI head registers no such service. It then callsMauiCultureStore.Save(culture)andMauiCultureStore.ApplyToProcess(culture)(:41-42), resolves the target asreturnPathor/when blank (:44), callsnavigation.NavigateTo(target, forceLoad: true)(:45), and returnsTask.CompletedTask(:47). - Why it's built this way: the ordering comment (
:37-40) is the load-bearing part: persist and activate BEFORE the reload, so the new culture is already the process default when the tree re-renders. Doing it the other way would reload under the old culture. The comment also points atMauiCultureStore'sApplyToProcessremarks for why assigningCurrentUICulturehere would pin the app to its startup language for the rest of the session. - Where it's used: registered scoped as
ICultureApplierbyUseMauiCulture()(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:130), which is itself already called byUseMauiDeviceCapabilities()(:41, folded in deliberately so no head can be left half-configured,:36-40) and must run afterAddUISharedso the plainAddoverrides thatTryAdddefault (:124-125). Consumed by the shared language-switcher UI.
MauiCultureInitializer
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Globalization·MMCA.Common.UI.Maui/Globalization/MauiCultureInitializer.cs:14· Level 2 · class (sealed)
- What it is: the startup half of hybrid-head localization (ADR-027): an
IMauiInitializeServicethat restores the persisted culture while the app is being built, before any window or page exists. - Depends on:
MauiCultureStore(ResolveplusApplyToProcess,MMCA.Common.UI.Maui/Globalization/MauiCultureInitializer.cs:22) and the MAUIIMauiInitializeServicehook (:14). - Concept introduced: the pre-window initialization hook.
IMauiInitializeService.Initializeruns insideMauiAppBuilder.Build(), so it is the earliest point at which app code can set process state, and the class doc names the two consequences that make it the right place (:5-8): the very first Blazor render already happens under the correct culture (no flash of the wrong language), and the user does not have to re-pick their language on every launch. It is the hybrid counterpart to the WASM head'sMmcaCultureBootstrap.[Rubric §27, i18n]assesses end-to-end language support including startup; without this, a hybrid head has no culture state of its own and always starts at the device locale, which is why persisting the choice inMauiCultureApplieralone is not enough (:9-12). - Walkthrough: one member.
Initialize(IServiceProvider services)(MMCA.Common.UI.Maui/Globalization/MauiCultureInitializer.cs:21-22) is an expression body callingMauiCultureStore.ApplyToProcess(MauiCultureStore.Resolve()). Theservicesparameter is unused, and the remarks say why (:17-20): the culture lives in device preferences and process state, both reachable without DI, so the parameter belongs to the interface rather than to this restore. - Why it's built this way: running before the container is meaningfully usable is exactly what forces
MauiCultureStoreto readPreferences.Defaultdirectly rather than through the asyncIDevicePreferencescontract; the two constraints (a synchronous hook and pre-window timing) are what shape the whole storage design. This same timing is why other singletons built during app construction must resolve their localized strings lazily rather than at construction:MauiBarcodeScannerService's doc records the identical hazard (MMCA.Common.UI.Maui/Capabilities/Media/MauiBarcodeScannerService.cs:17-21). - Where it's used: registered as a singleton
IMauiInitializeServicebyUseMauiCulture()(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:131), alongsideMauiCultureApplier(:130). MAUI invokes it duringMauiAppBuilder.Build(); nothing in app code calls it directly.
MauiGeocodingService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Geo·MMCA.Common.UI.Maui/Capabilities/Geo/MauiGeocodingService.cs:10· Level 2 · class (sealed)
- What it is: the MAUI adapter for
IGeocodingService: it turns a street address into aGeoPointusingGeocoding.Default, and returnsnullwhenever that cannot be done. - Depends on:
IGeocodingServiceandGeoPoint; MAUI EssentialsGeocoding; BCLFirstOrDefault. - Concept: no new contract idea here; this is the native half of the best-effort shape taught at
IGeocodingService. What is worth reading closely is the shape of the catch blocks, because it is the house pattern for every native adapter in this group: catch exactly the exceptions the platform is known to raise, translate them into the contract's neutral value, and let everything else propagate.[Rubric §29, Resilience & Business Continuity]assesses graceful degradation. A geocoder that is offline, times out, or is absent from the device produces "no proximity hint", never a failed render.[Rubric §15, Best Practices & Code Quality]assesses idiomatic error handling. A blanketcatchis deliberately absent: the exception filter namesTimeoutException,InvalidOperationException, andIOExceptionexplicitly (MauiGeocodingService.cs:30), so an unexpected exception type is still a bug that surfaces.
- Walkthrough
sealed class MauiGeocodingService : IGeocodingService(MauiGeocodingService.cs:10).IsSupported(MauiGeocodingService.cs:13): a constanttrue. Note the contrast with the geolocation sibling: geocoding needs no permission at all, because it is a network lookup rather than a device-location read (MauiGeocodingService.cs:6-7).GeocodeAsync(string address, CancellationToken = default)(MauiGeocodingService.cs:16): guards the address withArgumentException.ThrowIfNullOrWhiteSpace(:18), so a caller bug is still an exception; callsGeocoding.Default.GetLocationsAsync(address)(:22), takes the first result (:23), and projects itsLatitude/Longitudeinto aGeoPoint, ornullwhen there is no match (:24).- Two catch blocks translate platform failure into the contract's
null:FeatureNotSupportedExceptionfor a device with no geocoder (:26-29), and the filtered catch above, whose comment names the case as a geocoder that is unavailable or offline (:30-34). - The
cancellationTokenparameter is accepted but not forwarded:Geocoding.Default.GetLocationsAsynctakes no token.
- Why it's built this way: ADR-042. The domain model stores addresses and no coordinates, so geocoding exists purely to compute a presentation-time proximity hint; making every failure mode collapse to
nullkeeps that hint strictly optional and keeps the adapter free of any policy decision. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:53), overriding theNullGeocodingServicedefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:46). There is no browser implementation, so web heads keep the null default. - Caveats / not-in-source: which geocoding backend
Geocoding.Defaultresolves to (and therefore whether a lookup is billed or rate-limited) is a platform detail, not visible here.
MauiGeolocationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Geo·MMCA.Common.UI.Maui/Capabilities/Geo/MauiGeolocationService.cs:11· Level 2 · class (sealed)
- What it is: the MAUI adapter for
IGeolocationService: a soft, one-shot device-position read that prefers a fresh last-known fix, falls back to a timed current-position request, and yieldsnullon any refusal or failure. - Depends on:
IGeolocationServiceandGeoPoint; MAUI EssentialsGeolocation,Permissions,MainThread,GeolocationRequest,Location; on Android only,Android.Manifest.Permission. - Concept introduced, the two-tier freshness read and the partial-grant permission flow. This is the most behaviorally interesting native adapter in the unit, and it carries three mechanics the others do not.
- Freshness tiering. Asking the GPS for a current fix is slow and battery-expensive, so the adapter first takes the platform's last-known location and uses it only if it is younger than five minutes (
MauiGeolocationService.cs:13,:51-55,:76-77). Only when there is no fresh cached fix does it pay for a real read, and even then it caps the wait at ten seconds at medium accuracy (:14,:57), which is the right tier for a "roughly 3 km from the venue" hint. - Main-thread permission prompt, asked at most once per decided state. A platform permission dialog must be raised on the UI thread, so the prompt is marshalled through
MainThread.InvokeOnMainThreadAsyncwith astaticlambda (:38-39). The adapter checks the current status first (:24) and only prompts when the status is neitherGrantednorRestricted(:36), which is what keeps an already-decided permission from re-prompting on every read. - Approximate-location handling, and why
Restrictedcounts as a grant. On Android 12+, "Approximate only" grants coarse location and denies fine, and MAUI's compositeLocationWhenInUsecheck reports that combination asDenied. An Android-only block probes coarse alone through aCoarseLocationOnlypermission subclass and, when that is granted, rewrites the status toRestricted(:25-35). Both the prompt guard (:36) and the give-up guard (:46) then acceptGrantedorRestricted, because Essentials' ownGeolocationcalls accept the same pair and rejectingRestrictedwould turn the deliberate coarse design into a silent no-hint for every approximate user (:42-45). This is also the one place in the group where#if ANDROIDappears, and it appears inside an adapter: the platform conditional is confined to the head-specific package, so shared component code still sees onlyIGeolocationService. [Rubric §26, Front-End Security]assesses handling of sensitive, permission-gated capabilities. Location is requested with the narrowest scope available (Permissions.LocationWhenInUse,:24), only when needed, and a denial simply returnsnullrather than blocking or retrying.[Rubric §12, Performance & Scalability]assesses resource cost. The cached-fix-first path plus a bounded timeout means the common case costs nothing and the worst case is ten seconds, not an open-ended GPS acquisition.[Rubric §29, Resilience & Business Continuity]assesses degradation. Three distinct platform failures (unsupported device, location services switched off at the OS level, permission exception) all funnel to the same neutralnull(:61-73).
- Freshness tiering. Asking the GPS for a current fix is slow and battery-expensive, so the adapter first takes the platform's last-known location and uses it only if it is younger than five minutes (
- Walkthrough
- Two
static readonly TimeSpanpolicy constants:LastKnownFreshnessof five minutes (MauiGeolocationService.cs:13) andCurrentFixTimeoutof ten seconds (:14). Naming them rather than inlining the literals is what turns the freshness policy into something a reader can find. IsSupported(MauiGeolocationService.cs:17): a constanttrue.GetCurrentOrLastKnownAsync(CancellationToken = default)(MauiGeolocationService.cs:20) in order: checkPermissions.CheckStatusAsync<Permissions.LocationWhenInUse>()(:24); on Android, upgrade a coarse-only grant toRestricted(:25-35); prompt on the main thread when the status is neitherGrantednorRestricted(:36-40); returnnullif it is still neither (:46-49); read the last-known location and return it as aGeoPointwhenIsFresh(:51-55); otherwise issue aGeolocationRequest(GeolocationAccuracy.Medium, CurrentFixTimeout)and return that fix, ornull(:57-59). This is the one adapter in the unit that forwards the caller'scancellationTokento the platform call (:58).- Three catch blocks:
FeatureNotSupportedException(:61-64),FeatureNotEnabledExceptionwhose comment names location services switched off at the OS level (:65-69), andPermissionException(:70-73), each returningnull. IsFresh(Location location)(MauiGeolocationService.cs:76-77):location.Timestamp >= DateTimeOffset.UtcNow - LastKnownFreshness.CoarseLocationOnly(MauiGeolocationService.cs:85, inside#if ANDROID): a private sealedPermissions.BasePlatformPermissionsubclass whoseRequiredPermissionslistsAccessCoarseLocationalone (:87-88). It exists purely as a status probe, because the built-inLocationWhenInUselists both coarse and fine as required and therefore can never reportGrantedfor an approximate-only grant (:80-83).
- Two
- Why it's built this way: ADR-042. The contract promises that location never blocks a feature, so the adapter is written to fail toward
nullat every step; the freshness and timeout constants exist so a proximity hint can never become the slow part of a page; and the coarse probe exists so the platform's own partial-grant model does not silently degrade to no hint at all. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:52), overriding theNullGeolocationServicedefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:45). Web heads keep the null default. - Caveats / not-in-source: the Android manifest entries that make
AccessCoarseLocationand the fine-location permission requestable are declared by the head, not here.
DeepLinkRouteEventArgs
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Navigation·MMCA.Common.UI/Services/Capabilities/Navigation/DeepLinkRouteEventArgs.cs:4· Level 1 · class
- What it is: the event payload carrying an app-relative route requested by a native navigation source (a notification tap, a home-screen action, an app link, a QR scan) (
DeepLinkRouteEventArgs.cs:3). It is the argument type of the IDeepLinkDispatcherRouteRequestedevent. - Depends on:
System.EventArgs, which it derives from (DeepLinkRouteEventArgs.cs:4); nothing first-party. - Concept introduced: the classic .NET
EventArgs-derived payload for a typed event. It is immutable by construction: a constructor sets the singleRouteproperty, which is get-only (DeepLinkRouteEventArgs.cs:7,:10).[Rubric §25, Navigation & IA]§25 assesses coherent navigation. This type is the boundary object between native entry points and Blazor routing, carrying exactly one thing (an app-relative route) so every native source funnels through the same shape.
- Walkthrough: a
sealed class : EventArgs(DeepLinkRouteEventArgs.cs:4).- Constructor
DeepLinkRouteEventArgs(string route)(:7): assigns the route. Route(:10): the app-relative route to navigate to (for example/happening-now).
- Constructor
- Why it's built this way: a small dedicated
EventArgstype keeps the dispatcher's event strongly typed and lets the listener component read the route without casting, part of the single-funnel deep-link design (ADR-042). - Where it's used: declared as the payload of
IDeepLinkDispatcher.RouteRequested(MMCA.Common.UI/Services/Capabilities/Navigation/IDeepLinkDispatcher.cs:13), constructed inside DeepLinkDispatcher.Publish(MMCA.Common.UI/Services/Capabilities/Navigation/DeepLinkDispatcher.cs:44), and read by theDeepLinkListenercomponent's handler (MMCA.Common.UI/Components/Capabilities/DeepLinkListener.razor:32-36).
CapabilitiesJsModule
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/CapabilitiesJsModule.cs:12· Level 1 · class (sealed,IAsyncDisposable)
What it is: the shared, lazily-imported accessor for
capabilities-interop.js. Every browser capability adapter in this group goes through it, so one Blazor scope (a Server circuit or a WASM app) performs exactly one ES-module import no matter how many capabilities the page touches.Depends on:
IJSRuntimeand theJSDisconnectedException/JSExceptionpair (Microsoft.JSInterop,MMCA.Common.UI/Services/Capabilities/CapabilitiesJsModule.cs:1);LazyJsModule, the framework's import-once helper it wraps (:16,:19); BCLValueTask. Its consumers are the seven JS-backed adapters below.Concept introduced: this is the type that makes the whole browser leg of the capability layer safe, and it teaches two ideas at once.
- Prerender-safe JS interop. A Blazor component's first render can happen on the server with no browser attached (SSR prerender) and a Server circuit can be torn down mid-call. Calling into JS in either state throws. Rather than making every adapter (and every component) test for it, this class catches the whole JS-unavailable family and returns
default, so a capability call during prerender is simply a no-op that yieldsnullorfalse. - Import once per scope. An ES-module import is a network fetch plus an evaluation; doing it per capability call would be wasteful and would race.
LazyJsModulecaches the importedIJSObjectReference, and registering this class scoped (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:94) makes that cache per-circuit.
[Rubric §23, Front-End Performance]assesses whether the client avoids redundant work on the critical path; one shared module import instead of eight is exactly that.[Rubric §29, Resilience & Business Continuity]assesses graceful degradation; a disconnected circuit degrades a capability call to a silentdefaultrather than an unhandled exception in a render path.[Rubric §12, Performance & Scalability]applies because this is the single place the degradation policy is written, instead of eight copies of the same try/catch.- Prerender-safe JS interop. A Blazor component's first render can happen on the server with no browser attached (SSR prerender) and a Server circuit can be torn down mid-call. Calling into JS in either state throws. Rather than making every adapter (and every component) test for it, this class catches the whole JS-unavailable family and returns
Walkthrough
ModulePath(MMCA.Common.UI/Services/Capabilities/CapabilitiesJsModule.cs:14):./_content/MMCA.Common.UI/capabilities-interop.js, the static-web-asset path the RCL publishes.- The constructor (
:19) builds the innerLazyJsModuleover the host'sIJSRuntime; nothing is imported yet. InvokeOrDefaultAsync<T>(string identifier, object?[] args, CancellationToken)(:26-49): the single entry point. It awaitsGetOrImportAsync(cancellationToken)(:33, where the import happens on first use) and thenmodule.InvokeAsync<T>(identifier, cancellationToken, args)(:34). Three catch arms returndefault:InvalidOperationExceptionfor interop not yet available (SSR prerender before hydration,:36-40),JSDisconnectedExceptionfor a torn-down circuit (:41-44), andJSExceptionfor a browser API that itself threw (:45-48).DisposeAsync()(:52) forwards to the inner module's disposal, releasing theIJSObjectReferencewhen the scope ends.
Why it's built this way: the nullable-returning signature is the load-bearing choice. Because every export is invoked as
InvokeOrDefaultAsync<bool?>or<string?>, adapters can distinguish "JS said no" (false) from "JS never ran" (null), which is howBrowserConnectivityStatusServiceknows to retry its subscription after hydration. The class doc records that this mirrorsMauiBackNavigationBridge's degradation contract (:5-11), so both interop boundaries behave identically (ADR-042).Where it's used: registered scoped by
AddBrowserDeviceCapabilities()(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:94) and injected intoBrowserShareService,BrowserClipboardService,BrowserExternalLinkService,BrowserAccessibilityAnnouncer,BrowserConnectivityStatusService,BrowserDevicePreferences, andBrowserLocalCacheStore.Caveats / not-in-source: it swallows the JS failure without logging (it takes no
ILogger), so a genuinely broken export is indistinguishable at runtime from an unsupported browser API.
WebFormFactor
MMCA.Common.UI.Web ·
MMCA.Common.UI.Web.Services·MMCA.Common.UI.Web/Services/WebFormFactor.cs:12· Level 1 · class (sealed)
- What it is: the Blazor Server implementation of
IFormFactor, the two-method contract that lets shared UI ask which host it is running on. It reports the literal string"Web"because this code executes on the server, during SSR prerender and in interactive Server render mode (WebFormFactor.cs:6-7). - Depends on: first-party, only the
IFormFactorcontract it implements, imported fromMMCA.Common.UI.Services(WebFormFactor.cs:1,12). Externals:System.Environment(BCL) for the OS description. The type holds no app-specific state, which is exactly why it was hoisted out of the individual Blazor Web hosts into the shared package (WebFormFactor.cs:7-9). - Concept introduced: host-selected capability implementation, in its smallest possible form. One interface, and a different concrete class registered per host at DI composition time, is the mechanism the entire device-capability layer is built on;
IFormFactorshows it with a two-method contract and no platform API at all. The three bodies are this class for Blazor Server,WasmFormFactorin MMCA.Common.UI for WebAssembly, andMauiFormFactorin MMCA.Common.UI.Maui for the native head; the XML doc names all three so a reader lands on the family from any member (WebFormFactor.cs:8-9). Consuming components depend only on the interface, and the host composition root picks the body.[Rubric §18, UI Architecture]§18 assesses how cleanly presentation concerns are layered and how portable components are across render hosts. A two-method contract with three swappable bodies keeps every consuming component host-agnostic.[Rubric §22, Responsive / Cross-Browser]§22 assesses how the app adapts to device and environment.GetFormFactor()is the coarse signal a component branches on when server-rendered behavior must differ from WASM or native.
- Walkthrough: the class is
sealedand stateless, with no fields and no constructor (WebFormFactor.cs:12-19).GetFormFactor()is an expression-bodied member returning the constant"Web"(WebFormFactor.cs:15); it is a constant rather than a probe because Blazor Server always executes this code server-side, so there is nothing to detect.GetPlatform()returnsEnvironment.OSVersion.ToString()(WebFormFactor.cs:18), the server OS description. Both members carry<inheritdoc/>(WebFormFactor.cs:14,17), so the documented vocabulary for the return values lives once on the interface (MMCA.Common.UI/Services/IFormFactor.cs:9,12). - Why it's built this way: prerender and interactive Server render both run on the server, so no reliable client-device signal exists at this layer; answering
"Web"plus the server OS is the honest answer for this host rather than a guess about the browser. Keeping the type stateless and app-neutral is what allowed it to move up intoMMCA.Common.UI.Weband be shared by every Blazor Web host (ADR-042). - Where it's used: registered by the Blazor Server host through
AddCommonWebFormFactor(), which bindsIFormFactorto this class as a singleton (MMCA.Common.UI.Web/DependencyInjection.cs:47-48); the same XML doc points the WASM client atAddWasmFormFactor()from MMCA.Common.UI instead (MMCA.Common.UI.Web/DependencyInjection.cs:43-45). Both server heads call it once at startup: ADC atMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:96and Store atMMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/Program.cs:132. Resolved by any shared component that injectsIFormFactorto branch on the current host. - Caveats / not-in-source:
GetPlatform()reports the server OS, not the browser or the client device, so it must not be read as a client fingerprint. Because the registration is a plainAddSingleton, a head that also calledAddWasmFormFactor()in the same container would end up with twoIFormFactordescriptors and last-registration-wins resolution; nothing in this type guards against that.
MauiLocalNotificationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Notifications·MMCA.Common.UI.Maui/Capabilities/Notifications/MauiLocalNotificationService.cs:14· Level 2 · class (sealed)
- What it is: the MAUI adapter for
ILocalNotificationService: it schedules, replaces, and cancels on-device reminders through thePlugin.LocalNotificationpackage, with no backend involvement. - Depends on:
ILocalNotificationServiceandLocalNotificationRequest; thePlugin.LocalNotificationNuGet package (LocalNotificationCenter,NotificationRequest,NotificationRequestSchedule,MauiLocalNotificationService.cs:3-4). - Concept introduced, translation from a framework request record to a platform request object. Most adapters in this unit forward a primitive and translate a result. This one translates a whole payload in the other direction, which is exactly why
LocalNotificationRequestexists as a framework record: shared feature code never touchesNotificationRequest. Two mapping decisions are worth knowing.- The framework
Idbecomes the platformNotificationIdverbatim (MauiLocalNotificationService.cs:49), which is what makes the contract's "scheduling the same id replaces the pending entry" rule work: replacement is the platform's own id semantics, not extra bookkeeping here. - The optional
DeepLinkRouteis carried in the platform'sReturningDatafield, defaulted to an empty string when absent (:51). That is the payload the tap handler later reads. Crucially, this class does not route the tap: the class doc records that taps are routed toIDeepLinkDispatcherby the package bootstrap, not here (MauiLocalNotificationService.cs:11-12). [Rubric §26, Front-End Security]assesses permission-gated features. Permission maps to Android 13+POST_NOTIFICATIONSand iOS notification authorization (:8-9), and the adapter checks before it asks.[Rubric §17, DevOps]and[Rubric §32, Dependency & Supply-Chain]are both lightly relevant through one line of the class doc: scheduling uses inexact platform alarms deliberately, avoidingSCHEDULE_EXACT_ALARMbecause of Play policy (:9-10). That is a store-compliance constraint expressed in code, carried by a third-party package the framework pins.
- The framework
- Walkthrough
IsSupported(MauiLocalNotificationService.cs:17): a constanttrue.RequestPermissionAsync(CancellationToken = default)(MauiLocalNotificationService.cs:20): returnstrueimmediately ifAreNotificationsEnabled()already reports enabled (:23-26), otherwise callsRequestNotificationPermission()(:28). AnInvalidOperationExceptionis caught and reported as not permitted (:30-33).ScheduleAsync(LocalNotificationRequest request, CancellationToken = default)(MauiLocalNotificationService.cs:38): null-guards the request (:39); silently returns for a delivery time at or before now (:41-44), which is where the contract's "requests in the past are ignored" rule is actually enforced; builds the platformNotificationRequestfrom the four mapped fields plus aNotificationRequestSchedulecarryingNotifyTime(:46-56); then callsShowinside a try that swallowsInvalidOperationException, whose comment names the case as permission revoked mid-session, leaving the reminder a no-op (:58-65).CancelAsync(IReadOnlyCollection<int> ids, CancellationToken = default)(MauiLocalNotificationService.cs:70): null-guards (:71), skips the platform call entirely for an empty collection, and otherwise spreads the ids into the platformCancelwith a collection expression (:73-76). The work is synchronous behind an async signature, so it returnsTask.CompletedTask(:78).CancelAllAsync(CancellationToken = default)(MauiLocalNotificationService.cs:83):CancelAll()thenTask.CompletedTask(:84-85).- None of the four methods forwards its
CancellationToken; the underlying plugin calls take none.
- Why it's built this way: ADR-042 for the per-head selection. On-device reminders need no server round trip, so the entire capability is native-only and web heads keep
NullLocalNotificationService; routing the tap through the shared dispatcher rather than through this class keeps navigation in one place regardless of which native source produced the tap. - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:57), overriding the null default (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:50). The preferred host entry point isbuilder.UseMauiDeviceCapabilities()onHostingDependencyInjection, which additionally wires thePlugin.LocalNotificationlifecycle hooks this adapter relies on (MMCA.Common.UI.Maui/DependencyInjection.cs:26-30).
MauiPushRegistrationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities.Notifications·MMCA.Common.UI.Maui/Capabilities/Notifications/MauiPushRegistrationService.cs:16· Level 2 · class (sealed partial, primary constructor)
- What it is: the MAUI implementation of
IPushRegistrationService(ADR-044): it orchestrates registering this device with the server by asking the app for a platform push token, minting and remembering a stable installation id, and syncing that pair to the API'sNotifications/Devicesendpoints. - Depends on:
IPushDeviceTokenProvider(the app-supplied token source),IHttpClientFactory,IDevicePreferences(durable storage for the installation id), andILogger<MauiPushRegistrationService>, all four taken by the primary constructor (MauiPushRegistrationService.cs:16-20); plusPushDeviceToken,System.Net.Http.Json, and source-generated[LoggerMessage]partials. - Concept introduced, the orchestrator adapter. Every other native adapter in this unit wraps one platform static. This one wraps nothing: it is a composition of three injected services plus an HTTP call, and it is the type that makes the two-part push design from
IPushDeviceTokenProviderconcrete. The framework ships the whole registration pipeline (this class), the app ships the credentialed token source, and until it does,GetTokenAsyncreturnsnullandRegisterAsyncexits at its first check (MauiPushRegistrationService.cs:32-36). "Wired but inert" is not a special mode, it is just this early return.- The second idea here is the client-generated installation id. The device, not the server, mints a
Guid(:93) and persists it in device preferences under a fixed key (:21), so one physical install keeps one server-side device row across token rotations and re-registrations. UsingPUTrather thanPOSTagainstNotifications/Devices(:39-42) is what makes re-registration idempotent. [Rubric §29, Resilience & Business Continuity]assesses degradation: registration is explicitly a best-effort side channel that never throws (:11-13). Both public methods funnel every exception into a warning log and afalseor void return (:52-58,:77-82), so a failed registration can never break a login or a page render.[Rubric §13, Observability & Operability]assesses diagnostics quality. All three failure paths log through source-generated[LoggerMessage]methods at Warning (:98-105), and a rejected registration logs the actual HTTP status code (:46), which is the difference between a debuggable failure and a silent one.[Rubric §11, Security]assesses credential handling. The class holds no push credentials at all: it receives an already-minted platform token and posts it over the authenticated"APIClient"HTTP client (:38).
- The second idea here is the client-generated installation id. The device, not the server, mints a
- Walkthrough
- The primary constructor (
MauiPushRegistrationService.cs:16-20) takes the four dependencies;InstallationIdKeyis the fixed preference keymmca.push.installationId(:21). IsSupported(MauiPushRegistrationService.cs:25): a constanttrue.RegisterAsync(CancellationToken = default)(MauiPushRegistrationService.cs:28): gets the token and returnsfalseif there is none (:31-35); resolves or creates the installation id (:37); creates the named"APIClient"in ausing(:38);PUTs an anonymous body of installation id,Platform, andPushChannelto the relativeNotifications/DevicesURI (:39-42); logs and returnsfalseon a non-success status (:44-48), otherwisetrue(:50).UnregisterAsync(CancellationToken = default)(MauiPushRegistrationService.cs:63): reads the stored installation id and returns early when there is none (:66-70); otherwiseDELETEsNotifications/Devices/{id}with the id URL-escaped (:72-75). Note that it deliberately does not clear the stored id, so a later re-register reuses the same installation.GetOrCreateInstallationIdAsync(CancellationToken)(MauiPushRegistrationService.cs:86): returns the stored id when non-blank (:87-91), else generatesGuid.NewGuid().ToString("N"), persists it throughIDevicePreferences, and returns it (:93-95).- Three
[LoggerMessage]partial declarations at Warning level (MauiPushRegistrationService.cs:99-106): registration rejected with a status code, registration failed, unregistration failed.
- The primary constructor (
- Why it's built this way: ADR-044. Splitting "how do I get a token" (app-owned, credentialed) from "how do I tell the server about it" (framework-owned, this class) is what lets the framework ship a complete, tested push path that carries no vendor keys, and the registration comment in the composition root says exactly that (
MMCA.Common.UI.Maui/DependencyInjection.cs:64-66). - Where it's used: registered as a singleton by
AddMauiDeviceCapabilities()(MMCA.Common.UI.Maui/DependencyInjection.cs:67), overridingNullPushRegistrationService(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:61). The token provider it calls staysNullPushDeviceTokenProvider(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:62) unless the app registers a credentialed one. - Caveats / not-in-source: the server side of
Notifications/Devices(thePUT/DELETEhandlers and the device table) is not in this class or this chapter; it lives in the Notifications module. The"APIClient"named client's base address and auth handler are configured by the host, not here.
IDeepLinkDispatcher
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Navigation·MMCA.Common.UI/Services/Capabilities/Navigation/IDeepLinkDispatcher.cs:10· Level 2 · interface
- What it is: the single funnel between native navigation sources (notification taps, home-screen app actions, app links, QR scans) and Blazor routing (
IDeepLinkDispatcher.cs:3-9). Native code publishes an app-relative route; the sharedDeepLinkListenercomponent either receives it live or drains it from a pending buffer after a cold start. - Depends on: DeepLinkRouteEventArgs, the event payload;
System.EventHandler<T>otherwise. - Concept introduced: the live-event-or-buffered-cold-start handoff, the interesting mechanic of the deep-link design. When a listener is attached the route is raised live through
RouteRequested; when the app was cold-started by the tap and no listener exists yet, the route is buffered last-write-wins with capacity one forTryConsumePendingto drain after first render (IDeepLinkDispatcher.cs:5-9,:15-22). One interface covers both the warm and the cold navigation case.[Rubric §25, Navigation & IA]§25 assesses coherent, deep-linkable navigation. Every native entry point converges on this one contract, so routing behaves identically whether the app was already open or was launched by the link.[Rubric §19, State Management]§19 assesses where transient state lives. The pending route is a single-slot buffer owned by the dispatcher, a deliberately tiny piece of cross-render state rather than app-wide state.
- Walkthrough: three members.
RouteRequested(IDeepLinkDispatcher.cs:13): raised when a route is requested while a listener is attached; the contract documents that it runs on the publisher's thread, which is why the listener marshals onto the renderer.Publish(string)(:19): publishes a route request; with no listener attached the route is buffered, last-write-wins, capacity one.TryConsumePending(out string?)(:22): atomically takes the buffered pending route, if any.
- Why it's built this way: cold-start taps arrive before Blazor has rendered a listener, so a buffer is required to avoid dropping the launch route, and a single funnel keeps every native source consistent (ADR-042).
- Where it's used: implemented by DeepLinkDispatcher and consumed by the
DeepLinkListenercomponent, which subscribes and drains the buffer on first render (MMCA.Common.UI/Components/Capabilities/DeepLinkListener.razor:22-27) and unsubscribes on dispose (:30). Native publishers resolve it from the MAUI root service provider (MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:21,:40), and LocalNotificationRequest.DeepLinkRoutefeeds routes into it on notification tap.
DeepLinkDispatcher
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Navigation·MMCA.Common.UI/Services/Capabilities/Navigation/DeepLinkDispatcher.cs:9· Level 3 · class
- What it is: the default IDeepLinkDispatcher. It raises
RouteRequestedwhen a listener is attached, otherwise buffers the most recent route (capacity one) so a cold-start tap survives until the Blazor router renders (DeepLinkDispatcher.cs:3-8). Registered as a singleton so native callers can resolve it from the MAUI root provider. - Depends on: IDeepLinkDispatcher, the contract it implements, and DeepLinkRouteEventArgs, what it raises; the BCL
System.Threading.Locktype for its gate. - Concept introduced: reading the event handler inside the lock, and with it the modern
System.Threading.Lock. The obvious implementation snapshotsRouteRequestedinto a local, then takes a lock only to write the buffer. The source rejects that explicitly, and the comment records the interleaving it loses (DeepLinkDispatcher.cs:22-31):Publishsees no handler, the listener then subscribes, the listener drains an empty buffer, and only afterwards doesPublishwrite into a buffer nobody will read again, so the route is dropped. That race is real on a native head, where the callback thread and the first render are genuinely concurrent (the warm-boot deep link). Reading the handler and writing the buffer as one step under the same gate leaves only two orders: either the subscription was visible and the event fires, or it was not and the buffer write completes before the lock releases, so the listener'sTryConsumePending, which must take the same lock, finds the route. The invoke itself still happens outside the lock (:44) because a listener that navigates on that callback must not run under it.[Rubric §19, State Management]§19 assesses safe transient state. The single-slot_pendingRoute(:12) is read and cleared atomically inTryConsumePending(:50-54), so a buffered route is delivered exactly once.[Rubric §12, Performance & Scalability]§12 assesses lock discipline. The critical section is a field read and a field assignment; the handler invocation, which can trigger navigation and a render, is deliberately outside it.[Rubric §15, Best Practices & Code Quality]§15 assesses whether non-obvious code explains itself. The nine-line comment above the lock states the exact dropped-route interleaving, which is the kind of reasoning that is otherwise lost the first time someone "simplifies" the method.
- Walkthrough: fields, then the event, then two methods.
_gate(DeepLinkDispatcher.cs:11): aLockinstance, the typed C# 13 lock rather than locking on a plainobject._pendingRoute(:12): the single-slot buffer.RouteRequestedevent (:15): the implemented event.Publish(string)(:18-45): validates withArgumentException.ThrowIfNullOrWhiteSpace(:20), then insidelock (_gate)reads the handler (:35) and, when it isnull, stores the route and returns while still holding the gate (:37-40). With a handler present it falls through and invokes it outside the lock with a new DeepLinkRouteEventArgs (:44).TryConsumePending(out string?)(:48-57): takes and clears the pending route under the lock (:50-54) and returns whether one was present (:56).
- Why it's built this way: native taps can arrive on any thread and either before or after the listener attaches, so the dispatcher must be both thread-safe and cold-start-safe. A singleton with a locked single-slot buffer is the minimal design that satisfies both (ADR-042).
- Where it's used: registered as the singleton IDeepLinkDispatcher in
AddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:78); published into by the MAUI notification-tap bridge (MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:30-40); consumed by theDeepLinkListenercomponent; exercised byDeepLinkDispatcherTestsandDeepLinkListenerTests(see Group 27).
DependencyInjection
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:23· Level 4 · class (static,extension(IServiceCollection)block)
- What it is: the composition root for this entire group. One static class holding two registration helpers:
AddDeviceCapabilityDefaults()fills the container with a safe default for every capability contract, andAddBrowserDeviceCapabilities()overrides the subset a browser can really do (DependencyInjection.cs:15-22). - Depends on: every contract in this chapter, plus the
Fallbacksnamespace for the null defaults (DependencyInjection.cs:4) and theBrowsernamespace for the JS-interop implementations (:3);Microsoft.Extensions.DependencyInjectionand itsExtensionsnamespace for theTryAdd*helpers (:1-2). - Concept introduced: two-phase, last-registration-wins capability selection. This is the mechanism every other section in this chapter refers back to, so read the two phases as one story. Phase one:
AddUISharedcallsAddDeviceCapabilityDefaults()(MMCA.Common.UI/DependencyInjection.cs:143, under a comment stating the rule at:137-138), whichTryAdd-registers a null or neutral implementation for every contract (DependencyInjection.cs:40-78).TryAddis what makes this idempotent: a host that callsAddUISharedtwice does not double-register, and an app that pre-registered its own implementation beforeAddUISharedkeeps it (DependencyInjection.cs:16-20). Phase two: the head calls its own override helper afterAddUIShared, using a plainAddrather thanTryAdd, and because .NET DI resolves the last registration for a single-service request, the real implementation wins (DependencyInjection.cs:18-20). Browser overrides live in this same file; native overrides ship separately in theMMCA.Common.UI.Mauipackage asAddMauiDeviceCapabilities(DependencyInjection.cs:20-21), which is what keepsMMCA.Common.UIfree of any MAUI reference.[Rubric §1, SOLID]§1 assesses SOLID adherence. Open/Closed shows up concretely: adding a fourth head means adding a newAddXDeviceCapabilities()helper, not editing shared components or this defaults table.[Rubric §2, Design Patterns]§2 assesses whether classic patterns earn their keep. This is Strategy selected by the container, with Null Object as the default strategy (see NullGeocodingService for the null-object shape taught in full).[Rubric §22, Responsive / Cross-Browser]§22 assesses graceful behavior across heads. The point of the defaults table is that no shared component can ever fail to resolve a capability, whichever head it renders in, including during prerender before JS exists (:80-82).[Rubric §14, Testability]§14 assesses how easily the production wiring can be reproduced in a test.AddDeviceCapabilityDefaults()is public precisely so a consumer's bUnit base can register the same set the host gets, instead of hand-mirroring a list that rots the moment a new contract ships here (DependencyInjection.cs:28-35).[Rubric §33, Developer Experience]§33 assesses how obvious the right thing is to do. The ordering rule (overrides afterAddUIShared) is stated in the class XML doc rather than left to be discovered, and each non-obvious lifetime choice carries an inline comment at its registration.
- Walkthrough
public static class DependencyInjection(DependencyInjection.cs:23) with a singleextension(IServiceCollection services)block (:18), the C# extension-member syntax this workspace uses for DI registration throughout.AddDeviceCapabilityDefaults()(DependencyInjection.cs:37) is public, and the XML doc says why (:21-28): consumer bUnit test bases call it directly, which they do atMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/BunitTestBase.cs:26,MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/BunitTestBase.cs:18, andMMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Components/ComponentsSnapshotTests.cs:67. Production hosts still get it throughAddUIShared. The body is grouped by lifetime and rationale:- Stateless no-op singletons, seventeen
TryAddSingletoncalls (DependencyInjection.cs:40-56), covering connectivity, share, clipboard, haptics, map navigation, geolocation, geocoding, external links, text-to-speech, accessibility announcements, local notifications, screenshots, battery, biometrics, speech-to-text, the external auth broker, and the local cache store. - Push (ADR-044): IPushRegistrationService and IPushDeviceTokenProvider both default to inert (
:54-55), with the comment recording the deliberate split, UI.Maui overrides the registration service while the app overrides the token provider once real FCM or APNs credentials exist (:51-53). - Media picking (ADR-045): IMediaPickerService defaults to null because web heads render
InputFileinstead (:57-58). - Barcode scanning:
IBarcodeScannerServicedefaults to null because there is no browser primitive, and the native override is opt-in (UseCommonBarcodeScannerin UI.Maui), so even a MAUI head keeps the default until it asks for the camera (:60-63). IDevicePreferencesis the oneTryAddScoped(:67), and the comment says why: on Blazor Server the in-memory fallback must hold per-circuit (per-user) state, never cross-user state (:65-66).- IDeepLinkDispatcher is a singleton by contract (
:71) because native code publishes into it from outside any scope; web heads have no native publishers, so the shared buffer is simply inert there (:69-70).
- Stateless no-op singletons, seventeen
AddBrowserDeviceCapabilities()(DependencyInjection.cs:91) is what a Blazor Server or WebAssembly host calls afterAddUIShared. It registers CapabilitiesJsModule scoped first so all browser services share one JS module import per scope or circuit (:86-87), then overrides eight contracts withAddScoped(:89-96): share, clipboard, external links, accessibility announcer, connectivity, device preferences, local cache store, and map navigation. Everything else keeps its null default, which is why web heads have no geolocation, geocoding, speech, media picker, or local notifications.
- Why it's built this way: ADR-042 is cited on the class itself (
DependencyInjection.cs:16), with ADR-044 and ADR-045 cited inline for the push and media groups. TheTryAdd-then-Addordering is what lets one shared component tree run on three heads with zero host-detection code, and the scoped-versus-singleton split is driven by one question asked per contract: does this hold per-user state on a Blazor Server circuit? - Where it's used: called by
AddUIShared(MMCA.Common.UI/DependencyInjection.cs:143). The public browser helper is called by both web heads in each app: ADC atMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:61andMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web.Client/Program.cs:49, Store atMMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/Program.cs:99andMMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web.Client/Program.cs:39. - Caveats / not-in-source: the MAUI-side helper (
AddMauiDeviceCapabilities) is not in this file; it ships in theMMCA.Common.UI.Mauipackage, which is deliberately outsideMMCA.Common.slnxand built by dedicated windows jobs (ADR-042).
⬅ ADC Application Host, UI Shell & Cross-Module Composition • Index • Testing & Quality Infrastructure ➡