Onboarding guide
26. Device Capability Abstraction Layer (Native Contracts, MAUI, Browser & Fallback Adapters)
What this group covers. A single Blazor UI codebase in MMCA.Common.UI renders on three
very different heads: Blazor Server (server-side render + interactive Server circuits), Blazor
WebAssembly (the whole component tree running in 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, drop a local notification
and open the system share sheet; a server-rendered page can do none of that, and a WASM page can do
some of it through browser 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/geocoding, speech, push registration, media pick, 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 gets at DI composition time. This is the
[Rubric §18, UI Architecture] and [Rubric §22, Responsive/Cross-Browser] story in miniature, and the
whole design is ADR-042 (MMCA.Common/ADRs/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 records, never in a MAUI or
JS type. IBiometricAuthenticator
(MMCA.Common.UI/Services/Capabilities/IBiometricAuthenticator.cs:9) is the clearest example of the
house rule: availability and outcome are both plain Task<bool>, and every failure mode (cancel,
lockout, error) collapses to false 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 value type, not a platform one: GeoPoint
(MMCA.Common.UI/Services/Capabilities/GeoPoint.cs:9) is a sealed record latitude/longitude pair
that even carries its own haversine DistanceKmTo helper (GeoPoint.cs:17) so shared components
never touch a platform location type. PickedMedia,
PushDeviceToken, and LocalNotificationRequest
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:24), 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:27-60).
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. Browser heads call
AddBrowserDeviceCapabilities (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:73);
native heads call AddMauiDeviceCapabilities
(MMCA.Common.UI.Maui/DependencyInjection.cs:25), which ships in the separate MAUI-TFM package
MMCA.Common.UI.Maui (the one package built outside MMCA.Common.slnx, ADR-042). Both of those DI
classes use the C# extension(IServiceCollection) member idiom the codebase favours for registration
(see the primer). The lifetime choices are
deliberate: the browser services are Scoped (one per Blazor circuit) so per-user state never leaks
across circuits, while the MAUI services are Singleton because a native head is single-user and its
stateful services wrap app-global platform events.
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/Fallbacks/NullBiometricAuthenticator.cs:4) simply returns
false from both members, NullShareService,
NullClipboardService, and their siblings do nothing, and
AlwaysOnlineConnectivityStatusService reports permanent
connectivity because a server-rendered page has no offline concept. 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/DeviceInfo.Platform, and its siblings drive Essentials, the Community Toolkit,
and Plugin.LocalNotification.
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/Browser/CapabilitiesJsModule.cs:12), a lazy accessor for the
single capabilities-interop.js module (CapabilitiesJsModule.cs:14) registered once per circuit
(DependencyInjection.cs:76). Its InvokeOrDefaultAsync<T> (CapabilitiesJsModule.cs:27) 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:39-51). So BrowserShareService
(MMCA.Common.UI/Services/Capabilities/Browser/BrowserShareService.cs:8) calling navigator.share
degrades to "did not share" during prerender instead of throwing, exactly as a null implementation
would. This is the [Rubric §22, Responsive/Cross-Browser] and [Rubric §23, Front-End Performance]
discipline that lets the same component prerender on the server and hydrate in the browser without a
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 platform, 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,
WasmFormFactor (MMCA.Common.UI/Services/WasmFormFactor.cs:9) reports
"WebAssembly" for the browser runtime, and MauiFormFactor reports the real
device idiom. Each head registers its own (AddMauiFormFactor,
MMCA.Common.UI.Maui/DependencyInjection.cs:69, keeps this separate from the capability bundle so a
head can override just the form factor). The trio is the concrete illustration of why the whole group
exists: identical shared components read GetFormFactor()/GetPlatform() and adapt, and the
correct answer for "what am I running on" is injected, not detected inline.
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,
MMCA.Common/ADRs/043-mobile-deep-links-and-native-oauth-callback.md).
IDeepLinkDispatcher
(MMCA.Common.UI/Services/Capabilities/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; the shared
DeepLinkListener component (in the UI-components group) either receives it live via the
RouteRequested event or drains it from a buffer after first render. The default
DeepLinkDispatcher
(MMCA.Common.UI/Services/Capabilities/DeepLinkDispatcher.cs:9) is a singleton that solves the
cold-start race: when a tap launches the app before the router exists, Publish finds no attached
handler and stores the route in a single-entry buffer under a Lock (DeepLinkDispatcher.cs:18-34),
and the listener drains it via TryConsumePending once it renders (DeepLinkDispatcher.cs:37-46).
The event payload is DeepLinkRouteEventArgs. On MAUI the bridge is wired
by DeviceCapabilitiesInitializer
(MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:15), an IMauiInitializeService that hooks
LocalNotificationCenter.Current.NotificationActionTapped and republishes the tapped notification's
ReturningData route into the dispatcher (DeviceCapabilitiesInitializer.cs:28-42). That wiring is
installed by UseMauiDeviceCapabilities on the MauiAppBuilder
(MMCA.Common.UI.Maui/HostingDependencyInjection.cs:25), which also calls UseLocalNotification()
and registers the native capability bundle.
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,
MMCA.Common/ADRs/044-native-push-delivery.md) registers a real
IPushRegistrationService
(MMCA.Common.UI/Services/Capabilities/IPushRegistrationService.cs:10) on MAUI heads, but the
IPushDeviceTokenProvider defaults to a null provider that yields no
token, so even a native head stays registered-but-tokenless until the app supplies real FCM/APNs
credentials (DependencyInjection.cs:45-49). The IExternalAuthBroker
(MMCA.Common.UI/Services/Capabilities/IExternalAuthBroker.cs:10) defaults to
UnavailableExternalAuthBroker so web heads keep their existing
anchor-href OAuth flow, and the MAUI broker reports IsAvailable == false until the head configures
OAuth:MobileRedirectScheme (MMCA.Common.UI.Maui/DependencyInjection.cs:56-59). Biometrics stay on
their null default until ADR-042 Wave 4 lands (see the
DevicePreferenceKeys.AppLockEnabled key,
MMCA.Common.UI/Services/Capabilities/DevicePreferenceKeys.cs: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/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. It exposes an IsPersistent flag so a
head can hide device-settings UI where storage is ephemeral. The three implementations show the
lifetime story clearly: MauiDevicePreferences persists to native
Preferences, BrowserDevicePreferences persists to localStorage
through the shared JS module, and InMemoryDevicePreferences is
registered Scoped (DependencyInjection.cs:56) so the Blazor Server fallback holds per-circuit
state and reports IsPersistent == false. Never storing secrets here is a documented rule (tokens
belong in platform secure storage), which ties this into [Rubric §26, Front-End Security] and
[Rubric §11, Security].
Where this group sits. The capability contracts are consumed by the shared Blazor components and
pages (the UI-components group), by the connectivity/battery/accessibility banners, and by the deep-link
and notification surfaces. Nothing in this group references EF Core, the API, or a message broker: it
is pure 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), ADR-043 (deep links and native OAuth callback), ADR-044 (native push
delivery), and ADR-045 (managed file storage and avatars, the backing for the media-picker capability).
IFormFactor
MMCA.Common.UI ·
MMCA.Common.UI.Services·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/IFormFactor.cs:7· Level 0 · interface
- What it is: a two-method abstraction over device / hosting-environment detection, so the shared Razor component library can adapt behavior without knowing which host it is running inside. Each host (Blazor Server, WebAssembly, MAUI) supplies its own implementation (
IFormFactor.cs:4-6). - Depends on: nothing first-party. It is a pure contract with two
string-returning methods and no BCL dependency beyondSystem.String. Its concrete implementations are the host-specific siblings: WasmFormFactor (WebAssembly), WebFormFactor (Blazor Server), and MauiFormFactor (MAUI native). - Concept introduced: this is the first type in the device-capability layer, so it establishes the layer's whole shape. Define the capability as an interface in the shared UI library, then let each host register a concrete implementation at DI composition time. Shared components depend on the interface, never on
OperatingSystem.IsAndroid(),RuntimeInformation.ProcessArchitecture, or a#ifconditional, so one component tree serves all three hosts and stays unit-testable with a stub.[Rubric §18, UI Architecture]§18 assesses whether the presentation layer is componentized and host-agnostic rather than duplicated per platform.IFormFactorembodies it directly: the same component library is consumed by three hosts, and platform variance is pushed to a single injected boundary instead of being scattered through component markup.[Rubric §1, SOLID]§1 assesses interface segregation and dependency inversion. This is a minimal, focused interface (two methods, one concern) that shared components depend on abstractly, so the high-level UI does not depend on any concrete host detail.
- Walkthrough: two members, both returning
stringrather than an enum.GetFormFactor()(IFormFactor.cs:10): returns the device form factor, documented as one of"Web","WebAssembly", or"Phone".GetPlatform()(IFormFactor.cs:13): returns the platform / OS description. Thestringreturn types are deliberate: they let a new host or form factor appear without forcing a shared enum change that every assembly would have to recompile against.
- Why it's built this way: keeping the abstraction in
MMCA.Common.UI(which depends onSharedonly, per the layer rules inMMCA.Common/CLAUDE.md) means the shared component library carries no reference to any host-specific assembly. Each head owns and registers its own concrete class, the pattern used across the device-capability layer for biometric, geolocation, share, and the other native capabilities selected per host at composition time (ADR-042 covers the MAUI-native head's separate build/pack path). - Where it's used: registered per host through the
AddWasmFormFactor()/AddCommonWebFormFactor()/AddMauiFormFactor()extension methods (seeMMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:117for the WASM registration) and resolved by shared Blazor components that adapt layout or feature availability per platform. The Store and ADC WASM clients callAddWasmFormFactor()from theirProgram.cs(MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web.Client/Program.cs:65,MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web.Client/Program.cs:70).
WasmFormFactor
MMCA.Common.UI ·
MMCA.Common.UI.Services·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/WasmFormFactor.cs:9· Level 1 · class
- What it is: the WebAssembly implementation of IFormFactor. It reports
"WebAssembly"because this code runs entirely in the browser after the WASM runtime has loaded (WasmFormFactor.cs:4-5). - Depends on: IFormFactor (the contract it implements) and one BCL member,
System.Environment.OSVersion. It carries no app-specific state, which is why it was hoisted out of the individual app WASM clients into the shared UI library (WasmFormFactor.cs:6-7). - Concept introduced: this is the first concrete host adapter in the layer, so it shows the per-host implementation shape the interface set up. It is
sealed(WasmFormFactor.cs:9), a workspace default for leaf classes, and both methods are expression-bodied, single-line answers. Its siblings implement the same two methods differently: WebFormFactor reports"Web"plus the server OS, and MauiFormFactor reports the native device form factor.[Rubric §22, Responsive / Cross-Browser]§22 assesses whether the app adapts across the device and browser matrix. This adapter is the WASM leg of that story: it lets a shared component distinguish the browser-hosted WebAssembly runtime from a server-rendered or native host at runtime.
- Walkthrough: two members, both from
IFormFactor.GetFormFactor()(WasmFormFactor.cs:12): returns the constant string"WebAssembly".GetPlatform()(WasmFormFactor.cs:15): returnsEnvironment.OSVersion.ToString(), the browser-reported OS description.
- Why it's built this way: because the class holds no state and is host-generic, it lives in the shared
MMCA.Common.UIpackage rather than being copy-pasted into each app's WASM client. The Blazor Server and MAUI heads deliberately do not use it: they register their ownIFormFactor(WebFormFactorinMMCA.Common.UI.Web,MauiFormFactorinMMCA.Common.UI.Maui, the latter built on its own windows job per ADR-042). - Where it's used: registered as a singleton
IFormFactorbyAddWasmFormFactor()(MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:117-118), called from each WASM.Clienthost'sProgram.cs. Its behavior and singleton registration are covered byWasmFormFactorTests(MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/WasmFormFactorTests.cs:16).
DevicePreferenceKeys
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 (the doc comment references
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 §16, Maintainability] 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). - Why it's built this way: a
public const stringis compile-time inlined and usable in attribute/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/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·MMCA.Common.UI/Services/Capabilities/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 MAUILocation/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). EarthRadiusKm = 6371.0(GeoPoint.cs:11): the mean Earth radius constant the distance formula uses.DistanceKmTo(GeoPoint other)(GeoPoint.cs:17): null-guardsother, then computes the haversine great-circle distance in kilometers (GeoPoint.cs:19-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.
IAccessibilityAnnouncer
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 poll opening, a question being answered, the unread badge incrementing).
- Depends on: BCL only (
System.Threading.Tasks,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 + 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. - 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: implemented by
MauiAccessibilityAnnouncer,BrowserAccessibilityAnnouncer, and the inertNullAccessibilityAnnouncer; registered viaDependencyInjection. Called by live-update components in the head apps.
IBatteryStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 + 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, handlers re-read 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-6). Web and null fallbacks always reportfalseand never raise the event, 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.IsEnergySaverOn(IBatteryStatusService.cs:14): whether OS energy saver / 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; consumed by live/real-time components deciding whether to auto-join channels.
IBiometricAuthenticator
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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; returnstrueonly on positive verification, and cancellation, lockout, and errors all collapse tofalse(IBiometricAuthenticator.cs:15-17).
- Why it's built this way: folding cancellation/lockout/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; consumed by the auto-login app-lock gate. - 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".
IClipboardService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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, andNullClipboardService; the copy-link fallback path ofIShareService.
IConnectivityStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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-9): 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). Extends the property+event shape ofIBatteryStatusServicewith anInitializeAsyncbecause the browser adapter needs explicit JS listener setup. - Walkthrough
ConnectivityChanged(IConnectivityStatusService.cs:13): raised afterIsOnlinechanges.IsOnline(IConnectivityStatusService.cs:16): defaults totrueuntil known, 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:18-21).
- 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, and the Server defaultAlwaysOnlineConnectivityStatusService; consumed by the offline banner and request-skipping guards.
IDevicePreferences
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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) and the supported value types are exactlystring,bool,int,long,double,DateTimeOffset(IDevicePreferences.cs:7-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:13-16).GetAsync<T>(string key, T fallback, CancellationToken = default)(IDevicePreferences.cs:21): reads a value, returningfallbackwhen absent or unreadable.SetAsync<T>(string key, T value, CancellationToken = default)(IDevicePreferences.cs:25): best-effort write, storage failures are swallowed.RemoveAsync(string key, CancellationToken = default)(IDevicePreferences.cs:28): removes a value; unknown keys are ignored.
- 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, and the in-memory Server defaultInMemoryDevicePreferences; read/written by device-settings screens and the app-lock gate.
IExternalAuthBroker
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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-9). This is the client half of the native deep-link OAuth callback design (ADR-043): 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:15-19).
- 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; consumed by the login page's external-provider buttons.
IExternalLinkService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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:11-15).OpenAsync(Uri uri, CancellationToken = default)(IExternalLinkService.cs:19): opens the URI in the system browser / a new tab, best-effort.
- 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, andNullExternalLinkService; consumed by theExternalLinkcomponent (its fake counterpartFakeExternalLinkServicebacks the component tests).
IHapticFeedbackService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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:4-6), so a missing or throwing vibrator can never affect what the app does.IsSupportedisfalseon the web fallback. - 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 (e.g. a foregrounded notification arriving).
- 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; consumed by interactive components.
ILocalCacheStore
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 via the implementation).
- 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-8). 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. - 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.GetAsync<T>(string key, CancellationToken = default)(ILocalCacheStore.cs:20): reads and deserializes, or returnsdefault.RemoveAsync(string key, CancellationToken = default)(ILocalCacheStore.cs:23): removes an entry; unknown keys are ignored.
- 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, and the unavailableNullLocalCacheStore; consumed by offline-schedule components.
IMapNavigationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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. - 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, andNullMapNavigationService; consumed by venue/location components.
IPushRegistrationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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(Level 1, the token source it wraps). - 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 (the delete call is authenticated) (IPushRegistrationService.cs:4-9). The default implementation is a no-op on web heads. - 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:15-19).UnregisterAsync(CancellationToken = default)(IPushRegistrationService.cs:23): removes the installation, best-effort, called while still authenticated.
- 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; driven by the head apps' sign-in / sign-out lifecycle and syncing to the serverDevicesController(ADR-044).
IScreenshotService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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. - Concept, permissionless temp-file capture.
[Rubric §26, Front-End Security]and[Rubric §30, Compliance/Privacy]. Captured files land in the platform cache directory, never the photo library, so no storage permissions are needed (IScreenshotService.cs:4-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/null fallbacks:false).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; its output is handed toIShareService.ShareFileAsync.
IShareService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/IShareService.cs:8· Level 0 · interface
- What it is: opens the platform share affordance (native share sheet on MAUI,
navigator.sharein browsers) for a link or a local file. - Depends on: BCL only (
System.Uri); falls back toIClipboardService. - Concept, share with a copy-link fallback.
[Rubric §18, UI Architecture]. Both methods returnfalsewhen sharing is unavailable so callers can fall back toIClipboardServicecopy-link (IShareService.cs:4-6). This is the pairing that makes theboolreturn onIClipboardService.SetTextAsyncuseful. - Walkthrough
ShareLinkAsync(string title, Uri uri, CancellationToken = default)(IShareService.cs:11): shares a link with a title; returns whether a share UI was presented.ShareFileAsync(string title, string filePath, string contentType, CancellationToken = default)(IShareService.cs:17): shares a local file (e.g. a screenshot); returns whether a share UI was presented, and browser implementations reportfalse(no local file access,IShareService.cs:13-16).
- Why it's built this way: the boolean-return-plus-clipboard-fallback pattern lets a Share button
work everywhere: native heads present the sheet, browsers that lack
navigator.share(or file sharing) degrade to copying the link and confirming fromIClipboardService. - Where it's used: implemented by
MauiShareService,BrowserShareService, andNullShareService; consumesIScreenshotServiceoutput for image sharing andIClipboardServiceas its fallback.
ISpeechToTextService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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.UI(which referencesSharedalone, perMMCA.Common/CLAUDE.md) carries no dependency on any platform recognizer. - Concept introduced: the contract-per-capability shape and the two-phase last-wins registration were both established in the chapter overview and by IFormFactor; this type reuses them for speech input. The house-rule worth noting 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 a native-only affordance here, and the interface makes that variance a single injected boolean instead of a#ifin component markup.[Rubric §21, Accessibility]§21 assesses inclusive input/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). Thenull-on-failure contract is the same 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 a
Shared-only interface 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,MMCA.Common/ADRs/042-device-capability-abstraction.md). - Where it's used: registered as a singleton with the
NullSpeechToTextServicedefault inAddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:41); the MAUI-native override ships 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·MMCA.Common.UI/Services/Capabilities/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 where the platform can synthesize speech.[Rubric §27, i18n]§27 assesses localization depth. The contract documents culture-matched voice selection with a documented fall back to the default voice when none 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
Shared-only interface, real on MAUI and inert elsewhere (ADR-042). - Where it's used: registered with the
NullTextToSpeechServicedefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:35); MAUI overrides it. Consumed by components that read session and announcement text aloud.
LocalNotificationRequest
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/LocalNotificationRequest.cs:15· 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:3-6). It is the request record that keeps a platform notification type out of the shared contract, the same role GeoPoint, PickedMedia, and PushDeviceToken play for their capabilities. - 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 (for example a hash of a session id) so that rescheduling replaces rather than duplicates the pending entry (LocalNotificationRequest.cs:4-5,:7). This 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-delivery being ignored, the optional deep-link route).
- Walkthrough: a single positional
sealed record(LocalNotificationRequest.cs:15-20) with five members.Id(:16): the stable platform notification id; scheduling the same id replaces the pending entry.Title/Body(:17-18): already localized by the caller (the record does no i18n itself).DeliverAt(:19): absolute delivery time; requests in the past are ignored.DeepLinkRoute(:20): an optional app-relative route (for example/conference/sessions/42) published to IDeepLinkDispatcher when the user taps the notification, wiring the reminder 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/ILocalNotificationService.cs:22); the MAUI implementation 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. Not determinable from source here: which implementation drops them.
PickedMedia
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 result 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 to avoid that toolchain failure.[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/generated file name and 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 (not 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/IMediaPickerService.cs:15,:18); shared avatar-upload UI consumes the stream and disposes it.
PushDeviceToken
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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/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. 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.[Rubric §9, API & Contract Design]§9 assesses contract clarity. The record pins the two-field push handle shape and documents the exact platform token semantics per field.
- Walkthrough: a two-field
sealed record(IPushDeviceTokenProvider.cs:19).Platform(:17): the wire platform value (fcmv1orapns).Token(:18): the FCM registration token or 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 for native push).
- Where it's used: the return type of
IPushDeviceTokenProvider.GetTokenAsync(MMCA.Common.UI/Services/Capabilities/IPushDeviceTokenProvider.cs:13); the push registration service forwards it to the backend.
DeepLinkRouteEventArgs
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/DeepLinkRouteEventArgs.cs:4· Level 1 · class
- What it is: the event payload carrying an app-relative route requested by a native navigation source (notification tap, home-screen action, app link, QR scan) (
DeepLinkRouteEventArgs.cs:3). It is the argument type of the IDeepLinkDispatcherRouteRequestedevent. - Depends on:
System.EventArgs(it derives from it,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 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: raised through
IDeepLinkDispatcher.RouteRequested(MMCA.Common.UI/Services/Capabilities/IDeepLinkDispatcher.cs:13) and constructed inside DeepLinkDispatcher.Publish (DeepLinkDispatcher.cs:33); consumed by theDeepLinkListenercomponent in the shared layout.
IGeocodingService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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-GeoPointhalf 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 (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:11): whether the platform can geocode at all (web/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/optional concern, so it hides behind a
Shared-only interface with a null default and a native override selected per host (ADR-042). - Where it's used: registered with the
NullGeocodingServicedefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:33); consumed by venue-proximity UI.
IGeolocationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 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 cannot nag or hard-depend on a sensitive grant.
- Walkthrough: two members.
IsSupported(IGeolocationService.cs:10): 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
Shared-only contract with aNullGeolocationServicedefault (ADR-042). - Where it's used: registered with the
NullGeolocationServicedefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:32); consumed alongside geocoding for proximity hints.
ILocalNotificationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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. 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: 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 absence degrades to a silent no-op.[Rubric §24, Forms / Validation / UX Safety]§24 assesses safe state transitions. Re-scheduling by stable id (replace, not duplicate) prevents notification spam from repeated schedules.
- Walkthrough: five members.
IsSupported(ILocalNotificationService.cs:12): whether this platform can schedule local notifications.RequestPermissionAsync(CancellationToken)(:19): ensures permission, prompting if the platform requires consent and it is undecided, returning 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
Shared-only interface with aNullLocalNotificationServicedefault for web/server heads (ADR-042). - Where it's used: registered with the null default (
MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:37); the MAUI head implements real scheduling. The tapped-notification route flows into IDeepLinkDispatcher via the request'sDeepLinkRoute.
IMediaPickerService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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/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, working control.[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 pick is native-only, so it hides behind a
Shared-only contract with aNullMediaPickerServicedefault and a MAUI override (ADR-045 for media picking, ADR-042 for the layer). - Where it's used: registered with the
NullMediaPickerServicedefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:52); consumed by the shared avatar-upload UI, which disposes the returned PickedMedia stream after upload.
IPushDeviceTokenProvider
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 (Firebase messaging token on Android, 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, 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.
- Why it's built this way: separating the token provider (app-owned, credentialed) from the registration service (framework-owned, ADR-044) means the framework ships a complete push pipeline that stays dormant until an app drops in real FCM/APNs credentials.
- Where it's used: registered with the
NullPushDeviceTokenProviderdefault (MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:49); the app overrides it once credentials exist, and IPushRegistrationService forwards the token to the backend.
IDeepLinkDispatcher
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/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 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 via
RouteRequested; when the app was cold-started by the tap (no listener yet), the route is buffered last-write-wins with capacity one forTryConsumePendingto drain after first render (IDeepLinkDispatcher.cs:5-9,:16-22). One interface handles both the warm and cold navigation cases.[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 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; runs on the publisher's thread.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; a single funnel keeps every native source consistent (ADR-042).
- Where it's used: implemented by DeepLinkDispatcher and consumed by the shared
DeepLinkListenercomponent; native publishers resolve it from the MAUI root service provider. LocalNotificationRequest.DeepLinkRoute feeds routes into it on notification tap.
DeepLinkDispatcher
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/DeepLinkDispatcher.cs:9· Level 3 · class
- What it is: the default IDeepLinkDispatcher: 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 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: the snapshot-then-branch race-safe event raise, plus first use here of C# 13's
System.Threading.Lock.Publishsnapshots theRouteRequestedhandler into a local before checking it (DeepLinkDispatcher.cs:22-23), so a handler that detaches between the null check and the invoke cannot cause a torn call; if no handler is attached it stores the route under the lock (:25-28), otherwise it invokes the snapshot outside the lock (:33). The_gatefield is aLockinstance (DeepLinkDispatcher.cs:11), the modern typed lock rather than locking on anobject.[Rubric §19, State Management]§19 assesses safe transient state. The single-slot_pendingRoute(:12) is read-and-cleared atomically under the lock inTryConsumePending(:39-43), so a buffered route is delivered exactly once.[Rubric §12, Performance & Scalability]§12 assesses lock discipline. The handler is invoked outside the lock, keeping the critical section to a field assignment.
- Walkthrough: fields then methods.
_gate(DeepLinkDispatcher.cs:11) and_pendingRoute(:12): theLockand the single-slot buffer.RouteRequestedevent (:15): the implemented event.Publish(string)(:18-34): validates the route withArgumentException.ThrowIfNullOrWhiteSpace(:20), snapshots the handler, buffers under the lock when there is no listener, else invokes with a new DeepLinkRouteEventArgs.TryConsumePending(out string?)(:37-46): takes and clears the pending route under the lock and returns whether one was present.
- 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
IDeepLinkDispatcherinAddDeviceCapabilityDefaults(MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:60); exercised byDeepLinkDispatcherTestsandDeepLinkListenerTests(see Group 27).
DependencyInjection
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities·MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:16· Level 4 · class
- What it is: the device-capability registration entry point (
DependencyInjection.cs:8-15). It TryAdd-registers a safe default for every capability contract so shared components resolve them on any head, then lets heads override with plainAddregistrations, last registration wins for single-service resolution. - Depends on: the whole contract set in this namespace plus their
FallbacksandBrowserimplementations (DependencyInjection.cs:3-4);Microsoft.Extensions.DependencyInjectionand itsExtensions(forTryAdd*). It uses C#extension(IServiceCollection)members (:18), the workspace's DI-registration idiom (see primer). - Concept introduced: the two-phase, last-wins composition that is the load-bearing mechanism of this whole group, made concrete.
AddDeviceCapabilityDefaultsTryAdd-registers a neutral default for every contract (DependencyInjection.cs:24-63); becauseTryAddis a no-op when a service already exists, calling it repeatedly is idempotent, and because plainAddafter it wins for single-service resolution, a head layers its real implementations on top without unregistering anything.AddBrowserDeviceCapabilities(:73-88) is the browser override phase.[Rubric §2, Design Patterns]§2 assesses idiomatic patterns. This is the Null Object pattern at DI scale: every contract has an inert default, so no consumer ever resolves a missing service.[Rubric §18, UI Architecture]§18 assesses host-agnostic composition. Platform variance is confined to which registrations run at composition time; the component tree is identical across heads.[Rubric §1, SOLID]§1 assesses dependency inversion. Components depend on the contracts; the concrete family is chosen here, at the composition root.
- Walkthrough: two extension members on
IServiceCollection.AddDeviceCapabilityDefaults()(DependencyInjection.cs:24, internal): TryAdd-registers the null/neutral default for every contract. Most are stateless singletons (:27-52), for exampleISpeechToTextServicetoNullSpeechToTextService(:41) andIMediaPickerServicetoNullMediaPickerService(:52). Two deliberate exceptions:IDevicePreferencesis scoped so the Blazor Server fallback holds per-circuit (per-user) state, never cross-user state (:54-56), andIDeepLinkDispatcheris a singleton because native code publishes into it from outside any scope (:58-60). The push pair defaults to inert (:48-49), matching the inert-until-credentialed story of IPushDeviceTokenProvider.AddBrowserDeviceCapabilities()(DependencyInjection.cs:73, public): overrides the defaults with browser implementations (navigator.share, clipboard,aria-liveannouncements, online/offline watching,localStorage). Called afterAddUISharedfrom the Blazor Server and WebAssembly hosts. It first registers one scopedCapabilitiesJsModule(:76), a single JS-module import per scope/circuit shared by all browser services, then registers each browser service as scoped (:78-85). Every browser implementation is prerender-safe: JS-unavailable calls degrade to the null behavior instead of throwing (:69-72).
- Why it's built this way:
TryAdddefaults plus last-wins overrides let one shared library ship complete on every head while each head supplies only the implementations it can, and the browser services live here (not in a separate host) because they depend onSharedonly (ADR-042). Native overrides ship separately inMMCA.Common.UI.Maui(AddMauiDeviceCapabilities,DependencyInjection.cs:12-14) built on its own windows job (ADR-042). - Where it's used:
AddDeviceCapabilityDefaultsis called byAddUISharedin the wider UI group;AddBrowserDeviceCapabilitiesis called from each Blazor Server and WebAssembly host'sProgram.cs. Registration behavior is covered byCapabilityFallbackTests(MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/Capabilities/CapabilityFallbackTests.cs). - Caveats / not-in-source: the
internalvisibility ofAddDeviceCapabilityDefaults(DependencyInjection.cs:24) means it is called throughAddUISharedinside the same assembly, not directly by hostProgram.cs. Not determinable from this file: the exactAddUISharedcall site, which lives in the wider UI group'sDependencyInjection.
CapabilitiesJsModule
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/CapabilitiesJsModule.cs:12· Level 0 · class
- What it is - the single, prerender-safe JavaScript accessor that every browser capability adapter in this namespace shares. It lazily imports one ES module (
capabilities-interop.js) per Blazor scope/circuit and invokes its exports, swallowing the "JS is not reachable right now" exception family and returningdefaultinstead of throwing. - Depends on -
Microsoft.JSInterop(IJSRuntime,IJSObjectReference,JSDisconnectedException,JSException); implementsIAsyncDisposable(BCL). No first-party dependencies: it sits at the very bottom of the browser-adapter stack, which is why it is Level 0 while its consumers are Level 1. - Concept introduced - the browser degradation contract. This is the first place in the group where JS interop is bridged, so the mechanism is taught here and the adapters merely reference it. Blazor Server (and prerendered Blazor Web) runs component code on the server before the browser has a live circuit; during that window any
IJSRuntimecall throwsInvalidOperationException. A disposed or navigated-away circuit throwsJSDisconnectedException, and a browser API that itself rejects (permission denied, unsupported) surfaces asJSException.InvokeOrDefaultAsync(CapabilitiesJsModule.cs:27) catches all three (CapabilitiesJsModule.cs:39-51) and returnsdefault, so a caller on the server, on a dead circuit, or on a browser missing the API sees a benignnull/falserather than an exception bubbling into the render tree. The XML doc (CapabilitiesJsModule.cs:9-10) states this deliberately mirrors MauiBackNavigationBridge's contract: capability failures degrade, they never crash the page.[Rubric §22 - Responsive/Cross-Browser]§22 assesses whether the front end works across engines and rendering modes; this type is the lever, one accessor that keeps every capability call safe under SSR prerender and across browsers that lack a given API.[Rubric §29 - Resilience & Business Continuity]§29 assesses graceful degradation under partial failure; returningdefaulton a torn-down circuit (CapabilitiesJsModule.cs:44-51) is degradation by construction, not exception handling bolted on later.[Rubric §12 - Performance & Scalability]§12 assesses efficient resource use; the module import is memoized (_module ??= ...,CapabilitiesJsModule.cs:34) so the dynamicimport()runs at most once per instance.
- Walkthrough -
ModulePath(CapabilitiesJsModule.cs:14) is the static content URL./_content/MMCA.Common.UI/capabilities-interop.js, the_content/<PackageId>/convention by which a Razor Class Library ships static web assets to any consuming host.- The constructor (
CapabilitiesJsModule.cs:20) captures the hostIJSRuntime;_module(CapabilitiesJsModule.cs:17) starts null. InvokeOrDefaultAsync<T>(CapabilitiesJsModule.cs:27) is the one public entry point. On first call it imports the module viaInvokeAsync<IJSObjectReference>("import", ...)and caches the reference (CapabilitiesJsModule.cs:34-36), then invokes the requested export byidentifierwith the suppliedargs(CapabilitiesJsModule.cs:37). Every call threads the caller'sCancellationTokenand usesConfigureAwait(false).DisposeAsync(CapabilitiesJsModule.cs:55) releases the cachedIJSObjectReferenceif one was imported, and itself catchesJSDisconnectedException(CapabilitiesJsModule.cs:66-69) because a circuit that has already gone has nothing left to release.
- Why it's built this way - ADR-042 (device capability abstraction) establishes per-host adapters chosen at DI composition time; the browser host needs exactly one JS bridge so the eight adapters do not each import their own module or each re-implement the degradation try/catch. Centralizing both here keeps the adapters to a handful of lines and guarantees identical prerender behavior.
- Where it's used - injected into every Level-1 browser adapter in this group: BrowserAccessibilityAnnouncer, BrowserClipboardService, BrowserConnectivityStatusService, BrowserDevicePreferences, BrowserExternalLinkService, BrowserLocalCacheStore, and BrowserShareService. (BrowserMapNavigationService is the exception: it composes over
IExternalLinkServicerather than the JS module directly.) - Caveats / not-in-source - the module's actual exports (
announce,copyText,watchOnline,storageGet, and so on) live incapabilities-interop.js, a JavaScript asset outside this unit's type list; the adapters name the exports as string identifiers, but their JS bodies are not determinable from these.csfiles.
BrowserAccessibilityAnnouncer
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserAccessibilityAnnouncer.cs:8· Level 1 · class
- What it is - the browser implementation of IAccessibilityAnnouncer: it pushes a message into a visually hidden
aria-live="polite"region so screen readers speak status updates that carry no visible UI change. - Depends on - CapabilitiesJsModule; the interface IAccessibilityAnnouncer.
- Concept introduced - the live region. An
aria-live="polite"container is one every screen reader monitors; writing text into it queues that text for announcement without stealing focus. Per the XML doc (BrowserAccessibilityAnnouncer.cs:5-6) the region is created on first use bycapabilities-interop.js, so the app never has to add the element to its layout.[Rubric §21 - Accessibility]§21 assesses whether non-visual users receive equivalent information; routing programmatic status through a polite live region is the standard, focus-preserving way to do that, and this adapter is the browser end of it.
- Walkthrough - the constructor (
BrowserAccessibilityAnnouncer.cs:13) captures the shared module.AnnounceAsync(BrowserAccessibilityAnnouncer.cs:16) calls theannounceexport with the message (BrowserAccessibilityAnnouncer.cs:18); it awaits abool?but discards it, because on the server/prerender the degradation contract returns null and the announcement is simply a no-op. - Why it's built this way - ADR-042 splits accessibility announcement behind an interface so the MAUI host can route to the native platform while the browser host uses a live region; the adapter stays a two-line pass-through because the JS module owns the DOM detail.
- Where it's used - resolved wherever
IAccessibilityAnnounceris injected (status toasts, async-completion messaging) when the browser adapter set is selected at composition time.
BrowserClipboardService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserClipboardService.cs:4· Level 1 · class
- What it is - the browser implementation of IClipboardService: copies text to the OS clipboard via
navigator.clipboard.writeText. - Depends on - CapabilitiesJsModule; the interface IClipboardService.
- Walkthrough - the constructor (
BrowserClipboardService.cs:9) captures the module.SetTextAsync(BrowserClipboardService.cs:12) calls thecopyTextexport (BrowserClipboardService.cs:14-16) and collapses the nullable result withcopied == true(BrowserClipboardService.cs:17), so a null from the degradation contract (prerender, or a browser that blocks clipboard writes in an insecure context) reportsfalse. This== truenarrowing of abool?to a definite success/failure is the recurring pattern across the "did it work?" adapters in this group.[Rubric §22 - Responsive/Cross-Browser]§22 assesses cross-engine behavior; reporting a real boolean lets callers show a "copied" confirmation only when the write actually succeeded.
- Why it's built this way - ADR-042: clipboard is a device capability, abstracted so a native host uses the platform clipboard while the browser uses the async Clipboard API, both behind one interface.
- Where it's used - copy-to-clipboard affordances (copy-link, copy-code) resolved through
IClipboardService.
BrowserConnectivityStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserConnectivityStatusService.cs:11· Level 1 · class
- What it is - the browser implementation of IConnectivityStatusService: it reflects
navigator.onLineand raises an event when the window'sonline/offlineevents fire. It is the one browser adapter that needs a live JS-to-.NET callback, so it also implementsIAsyncDisposable. - Depends on - CapabilitiesJsModule;
Microsoft.JSInterop(DotNetObjectReference,[JSInvokable]); the interface IConnectivityStatusService;IAsyncDisposable(BCL). - Concept introduced - the JS-to-.NET callback via
DotNetObjectReference. Where the other adapters call into JS and return, connectivity has to be pushed from the browser: the window firesonline/offlineat any time. The adapter hands JavaScript aDotNetObjectReferenceto itself (BrowserConnectivityStatusService.cs:34) so the JS listeners can invoke a[JSInvokable].NET method whenever the state flips. This is the standard Blazor pattern for JS-originated events.[Rubric §19 - State Management]§19 assesses how UI state is held and propagated; connectivity is stateful (IsOnline) with change notification (ConnectivityChanged), and the adapter guards against redundant notifications.
- Walkthrough -
- Fields: the shared module, a nullable
_selfReference(theDotNetObjectReference), and a_watchinglatch (BrowserConnectivityStatusService.cs:13-15). ConnectivityChanged(BrowserConnectivityStatusService.cs:21) is the change event;IsOnline(BrowserConnectivityStatusService.cs:24) defaults totruewith a private setter, so the app assumes online until proven otherwise. The XML doc (BrowserConnectivityStatusService.cs:6-9) notes this "reports online untilInitializeAsyncruns" and directs callers to invoke it fromOnAfterRenderAsync, since it is a prerender-safe no-op before hydration.InitializeAsync(BrowserConnectivityStatusService.cs:27) is idempotent via_watching(BrowserConnectivityStatusService.cs:29-32), lazily creates_selfReference(BrowserConnectivityStatusService.cs:34), and calls thewatchOnlineexport passing that reference (BrowserConnectivityStatusService.cs:35-37). A null return means JS is not yet available, so it returns without latching to retry on a later call (BrowserConnectivityStatusService.cs:39-43); a non-null return latches_watchingand seeds the status (BrowserConnectivityStatusService.cs:45-46).OnBrowserConnectivityChanged(bool)(BrowserConnectivityStatusService.cs:51) is the[JSInvokable]callback target the JS listeners drive; its XML doc marks it "Not for app code."DisposeAsync(BrowserConnectivityStatusService.cs:54) tears the JS listeners down viaunwatchOnlinewhen watching, then disposes and nulls_selfReference(BrowserConnectivityStatusService.cs:56-62).UpdateStatus(bool)(BrowserConnectivityStatusService.cs:65) is the private funnel: it returns early when the value is unchanged (BrowserConnectivityStatusService.cs:67-70) and only then flipsIsOnlineand raisesConnectivityChanged, so subscribers are notified on genuine transitions only.
- Fields: the shared module, a nullable
- Why it's built this way - ADR-042 abstracts connectivity so a native host can use platform reachability APIs; on the web the only source is
navigator.onLineplus the window events, which require a callback object. The lifecycle (Initialize/Dispose) exists precisely because the browser adapter registers and must unregister real DOM listeners. - Where it's used - offline-aware UI (banners, retry gating) that subscribes to
ConnectivityChangedand readsIsOnlinethroughIConnectivityStatusService. - Caveats / not-in-source - the JS side registers/removes the window
online/offlinelisteners; that registration lives incapabilities-interop.js, not in this.csfile.
BrowserDevicePreferences
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserDevicePreferences.cs:10· Level 1 · class
- What it is - the browser implementation of IDevicePreferences: a typed key/value preference store backed by
localStorage, JSON-encoding each value under themmca.devicePrefs.key prefix. - Depends on - CapabilitiesJsModule;
System.Text.Json(JsonSerializer,JsonException); the interface IDevicePreferences. - Concept introduced - namespaced
localStoragewith fallback-on-failure. Every key is prefixed (KeyPrefix = "mmca.devicePrefs.",BrowserDevicePreferences.cs:12) so app preferences never collide with otherlocalStorageusers on the same origin.IsPersistent => true(BrowserDevicePreferences.cs:20) advertises that, unlike a native in-memory fallback, these values survive across sessions on the same browser profile (per the XML doc,BrowserDevicePreferences.cs:6-8).[Rubric §19 - State Management]§19 assesses persistence of user-scoped state; device preferences are exactly that, held client-side and durable per profile.
- Walkthrough -
GetAsync<T>(BrowserDevicePreferences.cs:23) validates the key (ArgumentException.ThrowIfNullOrWhiteSpace,BrowserDevicePreferences.cs:25), reads the raw string via thestorageGetexport (BrowserDevicePreferences.cs:27-29), and returns the caller'sfallbackwhen the value is missing (BrowserDevicePreferences.cs:30-33) or when JSON deserialization yields null or throwsJsonException(BrowserDevicePreferences.cs:35-43). So a corrupt or absent entry never surfaces as an error, it collapses to the caller's default.SetAsync<T>(BrowserDevicePreferences.cs:47) serializes the value and writes it viastorageSetunder the prefixed key (BrowserDevicePreferences.cs:51-54).RemoveAsync(BrowserDevicePreferences.cs:58) callsstorageRemovefor the prefixed key (BrowserDevicePreferences.cs:62-64).
- Why it's built this way - ADR-042: preferences are a capability so the MAUI host uses platform secure/preference storage while the browser uses
localStorage; the fallback-on-failure semantics keep the interface total (a get always returns aT). - Where it's used - components reading and writing durable per-device settings (theme, density, dismissed hints) through
IDevicePreferences. - Caveats / not-in-source - this shares the
storageGet/storageSet/storageRemoveexports with BrowserLocalCacheStore; the two differ only by key prefix, so a preference and a cache entry with the same logical key do not collide.
BrowserExternalLinkService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserExternalLinkService.cs:8· Level 1 · class
- What it is - the browser implementation of IExternalLinkService: it opens a URL in a new tab via
window.open, and declares that it does not intercept anchor navigation. - Depends on - CapabilitiesJsModule; the interface IExternalLinkService;
Uri(BCL). - Concept introduced -
InterceptsLinksas a host capability flag.InterceptsLinks => false(BrowserExternalLinkService.cs:16) tells callers that on the web, ordinary<a target="_blank">anchors already do the right thing, so components should not route markup links through this service; only programmatic opens (for example the maps fallback) needOpenAsync. A native host, by contrast, would returntrueand take over link handling. The boolean lets one component template serve both hosts.[Rubric §26 - Front-End Security]§26 assesses safe handling of outbound navigation; concentrating programmatic external opens behind one service is where a host would centralizenoopener/allowlist policy.
- Walkthrough - the constructor (
BrowserExternalLinkService.cs:13) captures the module.OpenAsync(Uri, ...)(BrowserExternalLinkService.cs:19) null-checks the URI (BrowserExternalLinkService.cs:21) and calls theopenExternalexport with the string form (BrowserExternalLinkService.cs:23-25), discarding the result since a prerender no-op is acceptable. - Why it's built this way - ADR-042 abstracts external-link opening; the
InterceptsLinksflag exists so shared components can honor native anchor behavior on the web and delegated behavior on native without branching on the host type. - Where it's used - BrowserMapNavigationService composes over it; more broadly, any component opening an outbound URL programmatically resolves
IExternalLinkService.
BrowserLocalCacheStore
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserLocalCacheStore.cs:10· Level 1 · class
- What it is - the browser implementation of ILocalCacheStore: a typed JSON document cache over
localStorage, using themmca.localCache.key prefix for small offline snapshots. - Depends on - CapabilitiesJsModule;
System.Text.Json; the interface ILocalCacheStore. - Concept introduced - this is the second
localStorage-backed adapter; the namespaced-storage mechanism is introduced under BrowserDevicePreferences. The difference is intent and the miss semantics: where preferences take a caller-suppliedfallback, the cache returnsdefaulton a miss.IsAvailable => true(BrowserLocalCacheStore.cs:20) advertises that a store exists (the native inert fallback would report false). The XML doc (BrowserLocalCacheStore.cs:6-8) flags the ~5 MBlocalStoragecap and advises lean documents.[Rubric §12 - Performance & Scalability]§12 assesses client-side resource limits; the 5 MB origin quota is the constraint this store lives within, which is why it is scoped to small snapshots rather than a general cache.
- Walkthrough -
SetAsync<T>(BrowserLocalCacheStore.cs:23) validates the key (BrowserLocalCacheStore.cs:25), serializes to JSON, and writes viastorageSetunder the prefix (BrowserLocalCacheStore.cs:27-30).GetAsync<T>(BrowserLocalCacheStore.cs:34) reads viastorageGet(BrowserLocalCacheStore.cs:38-40), returnsdefaulton a miss (BrowserLocalCacheStore.cs:41-44), and on aJsonExceptionalso returnsdefault(BrowserLocalCacheStore.cs:46-53) so a schema change that invalidates an old cached document reads as a cache miss rather than an error.RemoveAsync(BrowserLocalCacheStore.cs:57) callsstorageRemoveunder the prefix (BrowserLocalCacheStore.cs:61-63).
- Why it's built this way - ADR-042: an offline cache is a capability so the native host can use a richer store while the browser uses
localStorage. The distinctmmca.localCache.prefix keeps cache documents from colliding with preferences (mmca.devicePrefs.) even under identical logical keys. - Where it's used - components caching small read-model snapshots for offline/optimistic display through
ILocalCacheStore.
BrowserMapNavigationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserMapNavigationService.cs:7· Level 1 · class
- What it is - the browser implementation of IMapNavigationService: with no native maps app on the web, it opens a Google Maps search for an address in a new tab.
- Depends on - IExternalLinkService (not the JS module directly);
Uri(BCL). This is the one adapter in the unit built by composition over another capability rather than over CapabilitiesJsModule. - Concept introduced - composing one capability over another. Rather than reach for JS interop, this adapter takes
IExternalLinkService(BrowserMapNavigationService.cs:15, ctorBrowserMapNavigationService.cs:18) and builds a Maps URL to hand it. Layering capabilities keeps the "open a URL" policy (new tab, security) in one place and lets maps navigation be pure URL construction.[Rubric §1 - SOLID]§1 assesses dependency direction and single responsibility; depending on theIExternalLinkServiceabstraction rather than duplicating window-open logic is dependency inversion plus reuse in one move.
- Walkthrough -
MapsSearchUrl(BrowserMapNavigationService.cs:12) is the public Google Maps search endpointhttps://www.google.com/maps/search/?api=1&query=. The surrounding#pragma warning disable S1075(BrowserMapNavigationService.cs:11-13) documents that this hard-coded URL is intentional: the public endpoint is the integration point on the web, there is no environment-specific path to externalize.OpenAddressAsync(string, string?, ...)(BrowserMapNavigationService.cs:22) validates the address (BrowserMapNavigationService.cs:24), URL-encodes it withUri.EscapeDataStringand appends it to the search endpoint (BrowserMapNavigationService.cs:26), delegates to_externalLinkService.OpenAsync(BrowserMapNavigationService.cs:27), and returnstrue(BrowserMapNavigationService.cs:28).
- Why it's built this way - ADR-042: map navigation is abstracted so the MAUI host launches the platform maps app while the browser opens a Maps search URL; reusing
IExternalLinkServicemeans the browser adapter inherits whatever new-tab/open policy that service enforces. - Where it's used - venue/location UI ("open in maps") resolved through
IMapNavigationService. - Caveats / not-in-source - the
labelparameter is accepted but not used in the browser URL (the Google Mapssearchendpoint keys off thequeryonly); it exists to satisfy the interface shape that native hosts use.
BrowserShareService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Browser·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Browser/BrowserShareService.cs:8· Level 1 · class
- What it is - the browser implementation of IShareService: it shares a link through the Web Share API (
navigator.share) and reports whether the platform actually shared, so callers can fall back to copy-link when it did not. - Depends on - CapabilitiesJsModule; the interface IShareService;
Uri(BCL). - Concept introduced - feature-detected sharing with an honest boolean. The Web Share API is absent on desktop Firefox and in insecure contexts (per the XML doc,
BrowserShareService.cs:5-6).ShareLinkAsyncreturns a realboolso a caller can offer a copy-link fallback when sharing is unavailable, rather than silently doing nothing.[Rubric §22 - Responsive/Cross-Browser]§22 assesses graceful behavior where a browser lacks an API; the boolean return plus the degradation contract is the cross-browser fallback mechanism in miniature.
- Walkthrough -
ShareLinkAsync(string, Uri, ...)(BrowserShareService.cs:16) null-checks the URI (BrowserShareService.cs:18), calls theshareLinkexport with title and string URI (BrowserShareService.cs:20-22), and narrows the nullable result withshared == true(BrowserShareService.cs:23), so an unavailable API or a prerender no-op reportsfalse.ShareFileAsync(...)(BrowserShareService.cs:27) is a hardTask.FromResult(false): file sharing is unsupported on the browser adapter, stated in code rather than by throwing, so callers can branch on the result.
- Why it's built this way - ADR-042: sharing is a capability so the native host uses the OS share sheet (including files) while the browser uses
navigator.sharefor links only; returningfalsefromShareFileAsynckeeps the interface total and steers browser callers to a supported path. - Where it's used - "share" affordances (share a session, share a link) resolved through
IShareService, with copy-link as the documented fallback when it returnsfalse.
MauiAccessibilityAnnouncer
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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) viaSemanticScreenReader.Default. - Depends on:
IAccessibilityAnnouncer(the contract it implements); MAUI EssentialsSemanticScreenReader; BCLTask/FeatureNotSupportedException. - Concept: this is the first of fifteen per-host adapters in this unit, so it is worth stating the shared shape once. Each class wraps exactly one narrow capability interface (introduced in this group's interface unit) over the platform SDK, and is selected at DI composition time on the native head.
[Rubric §21, Accessibility]assesses whether non-visual users get equivalent information; this adapter routes the announcement through the OS assistive layer and is a deliberate silent no-op when none is active.[Rubric §2, Design Patterns]: adapter + Null-Object, the pairing repeated across the whole capability family. - Walkthrough:
AnnounceAsync(string message, CancellationToken = default)(MauiAccessibilityAnnouncer.cs:12) calls the synchronousSemanticScreenReader.Default.Announce(message)(MauiAccessibilityAnnouncer.cs:16), swallowsFeatureNotSupportedExceptionwhen no screen-reader integration exists on the platform (MauiAccessibilityAnnouncer.cs:18-21), and returnsTask.CompletedTask(MauiAccessibilityAnnouncer.cs:23). The platform API is fire-and-forget 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 (they never branch on "is a screen reader present"); the completed-task return avoids a needless thread hop for a synchronous OS call.
- Where it's used: registered for native heads alongside its siblings
BrowserAccessibilityAnnouncerandNullAccessibilityAnnouncer; consumed by live-update components announcing events a sighted user perceives only visually.
MauiBatteryStatusService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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 overBattery.Default. - Depends on:
IBatteryStatusService; MAUI EssentialsBattery/EnergySaverStatus; BCLIDisposable/EventHandler. - Concept: the property-plus-change-event capability shape (introduced by
IBatteryStatusService) meets a subscription-lifetime concern. This is a singleton that hooks a platform event in its constructor and unhooks inDispose, so it must live and die with the container.[Rubric §12, Performance & Scalability]and[Rubric §23, Front-End Performance]assess whether the client adapts work to device constraints; here live features can throttle when the OS reports low power. - Walkthrough
- The constructor (
MauiBatteryStatusService.cs:12) subscribesOnEnergySaverStatusChangedtoBattery.Default.EnergySaverStatusChanged, so it observes transitions for its whole lifetime. EnergySaverChanged(MauiBatteryStatusService.cs:16): the contract event, re-raised from the platform handler.IsEnergySaverOn(MauiBatteryStatusService.cs:19): readsBattery.Default.EnergySaverStatus == EnergySaverStatus.Onon each access, so subscribers re-read the live value rather than trusting event args.Dispose()(MauiBatteryStatusService.cs:22) unsubscribes from the platform event, preventing a leak against the process-lifetimeBatterystatic.OnEnergySaverStatusChanged(...)(MauiBatteryStatusService.cs:24) forwards the platform notification as the contract's argument-freeEnergySaverChanged.
- The constructor (
- Why it's built this way: subscribing in the constructor and unsubscribing in
Disposeis the correct lifetime for a static platform event held by a DI singleton; re-reading the property (rather than caching the args) keeps a single source of truth for the current state. - Where it's used: registered for native heads; its fallback sibling is
NullBatteryStatusService. Consumed by live/real-time components deciding whether to auto-join channels.
MauiBiometricAuthenticator
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiBiometricAuthenticator.cs:13· Level 1 · class (sealed)
- What it is: the platform-direct adapter for
IBiometricAuthenticator(ADR-042 Wave 4), driving the AndroidXBiometricPrompton Android,LAContexton iOS/MacCatalyst, and reporting unavailable on Windows. - Depends on:
IBiometricAuthenticator; per-platform SDKs behind compilation symbols (AndroidXBiometricManager/BiometricPrompt/FragmentActivity,LocalAuthentication.LAContext); BCLTaskCompletionSource,MainThread. - Concept: fail-closed boolean auth gating (introduced by
IBiometricAuthenticator) realized with real platform prompts.[Rubric §11, Security]and[Rubric §26, Front-End Security]assess whether client-side auth degrades safely; every negative outcome here (cancel, lockout, error, 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 body is#if-partitioned per target so each head compiles only its own SDK. - Walkthrough
- Android (
MauiBiometricAuthenticator.cs:15-69):AllowedAuthenticatorscombinesBiometricWeak | DeviceCredential(MauiBiometricAuthenticator.cs:16-18), so a device PIN/pattern satisfies the prompt when no biometric is enrolled.IsAvailableAsync(:21) mapsBiometricManager.CanAuthenticatetoBiometricSuccess.AuthenticateAsync(:29) requires aFragmentActivity(elsefalse,:31-34), builds aTaskCompletionSource, and on the main thread (:38) resolves the main executor (falseif null,:41-45), then shows aBiometricPrompttitledreason(:47-52). Cancellation is wired to resolvefalse(:55). The nestedAuthenticationCallback(:59) setstrueon success (:62) andfalseon error (:65), but deliberately does not complete onOnAuthenticationFailedbecause a single bad attempt leaves the prompt up (:68). - iOS / MacCatalyst (
MauiBiometricAuthenticator.cs:70-92): both methods useLAContextwithLAPolicy.DeviceOwnerAuthentication(Face ID / Touch ID with passcode fallback), returning the policy-evaluation result. - Other heads (Windows,
MauiBiometricAuthenticator.cs:93-101): both methods returnTask.FromResult(false), because the unpackaged WinUI head cannot presentUserConsentVerifier.
- Android (
- Why it's built this way: folding every non-success into
falseforbids a partial-success path at the call site; allowing device-credential as well as biometric means a user without enrolled biometrics can still pass the app lock; not completing on a single failed attempt matches the platform prompt's own retry loop. - Where it's used: registered for native heads; the inert fallback is
NullBiometricAuthenticator. Consumed by the stored-token auto-login app-lock gate, toggled throughDevicePreferenceKeys. - Caveats / not-in-source: the token store and auto-login flow live in the head apps and the Identity layer; this class only answers "is the enrolled user present now".
MauiClipboardService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiClipboardService.cs:6· Level 1 · class (sealed)
- What it is: the MAUI adapter for
IClipboardService, writing text to the system clipboard overClipboard.Default. - Depends on:
IClipboardService; MAUI EssentialsClipboard; BCLFeatureNotSupportedException. - Concept: best-effort capability with a success return (introduced by
IClipboardService).[Rubric §18, UI Architecture]: theboolresult is what lets a caller confirm with a snackbar only when the write actually landed. - Walkthrough:
SetTextAsync(string text, CancellationToken = default)(MauiClipboardService.cs:9) awaitsClipboard.Default.SetTextAsync(text)and returnstrue(MauiClipboardService.cs:13-14), or returnsfalseonFeatureNotSupportedException(MauiClipboardService.cs:16-19). - Why it's built this way: reporting success (not
void) makes this adapter the copy-link fallback signal forIShareServicecallers when a native share sheet is unavailable. - Where it's used: registered for native heads next to
BrowserClipboardServiceandNullClipboardService; the copy-link fallback path ofIShareService.
MauiConnectivityStatusService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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 overConnectivity.Current. - Depends on:
IConnectivityStatusService; MAUI EssentialsConnectivity/NetworkAccess; BCLIDisposable/ValueTask. - Concept: offline-awareness at the UI edge (introduced by
IConnectivityStatusService), same singleton subscription lifetime asMauiBatteryStatusService.[Rubric §29, Resilience & Business Continuity]assesses graceful degradation; the offline banner and request-skipping guards read from here. - Walkthrough
- The constructor (
MauiConnectivityStatusService.cs:14) subscribesOnPlatformConnectivityChangedtoConnectivity.Current.ConnectivityChanged. ConnectivityChanged(MauiConnectivityStatusService.cs:18): the contract event.IsOnline(MauiConnectivityStatusService.cs:21): readsConnectivity.Current.NetworkAccess == NetworkAccess.Internet. This is the load-bearing detail: a captive-portal ("constrained") network is treated as offline because the API gateway is unreachable there, which is exactly what the offline banner should say.InitializeAsync(...)(MauiConnectivityStatusService.cs:24) returnsValueTask.CompletedTask, because the native adapter subscribes in its constructor and needs no post-render JS listener setup (unlike the browser adapter).Dispose()(MauiConnectivityStatusService.cs:27) unsubscribes;OnPlatformConnectivityChanged(MauiConnectivityStatusService.cs:29) forwards the event.
- The constructor (
- Why it's built this way: mapping only full
Internetaccess to online (not merely "some network") makes the banner honest about gateway reachability; the no-opInitializeAsynckeeps the always-ready native adapter allocation-free while satisfying the browser-driven contract. - Where it's used: registered for native heads; siblings are
BrowserConnectivityStatusServiceand the Server defaultAlwaysOnlineConnectivityStatusService. Consumed by the offline banner and request-skipping guards.
MauiDevicePreferences
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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 byPreferences.Default. - Depends on:
IDevicePreferences; MAUI EssentialsPreferences; BCLSystem.Text.Json. - Concept: device-scoped client state (introduced by
IDevicePreferences; keys fromDevicePreferenceKeys).[Rubric §19, State Management]assesses whether state has a clear owner and lifetime; these values describe this device and never roam.[Rubric §26, Front-End Security]: the doc comment forbids secrets here (those belong inSecureStorage). - Walkthrough
KeyPrefix = "mmca.devicePrefs."(MauiDevicePreferences.cs:14): every key is namespaced under a shared prefix, mirroring the browser adapter so key/value semantics hold on every head.IsPersistent(MauiDevicePreferences.cs:17):true, values survive an app restart.GetAsync<T>(...)(MauiDevicePreferences.cs:20): guards the key, reads the prefixed raw string (returningfallbackwhen absent,:24-28), then JSON-deserializes, returningfallbackon a null result or onJsonException(MauiDevicePreferences.cs:30-38).SetAsync<T>(...)(MauiDevicePreferences.cs:42): JSON-serializes the value and writes it under the prefixed key.RemoveAsync(...)(MauiDevicePreferences.cs:51): removes the prefixed key.
- 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; deserialization failures degrade to the caller's
fallbackrather than throwing into a render path. - Where it's used: registered for native heads; siblings are
BrowserDevicePreferencesand the in-memory Server defaultInMemoryDevicePreferences. Read/written by device-settings screens and the app-lock gate.
MauiExternalLinkService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiExternalLinkService.cs:10· Level 1 · class (sealed)
- What it is: the MAUI adapter for
IExternalLinkService, opening external URLs in the system browser (or OS handler) becausetarget="_blank"dead-ends inside a BlazorWebView. - Depends on:
IExternalLinkService; MAUI EssentialsBrowser/Launcher; BCLUri. - Concept: the WebView dead-link workaround (introduced by
IExternalLinkService).[Rubric §25, Navigation & IA]and[Rubric §18, UI Architecture]. - Walkthrough
InterceptsLinks(MauiExternalLinkService.cs:13):true, because links must be routed throughOpenAsyncinside the WebView rather than rendered as raw anchors.OpenAsync(Uri uri, CancellationToken = default)(MauiExternalLinkService.cs:16): null-guardsuri; forhttp/httpsschemes it usesBrowser.Default.OpenAsync(..., BrowserLaunchMode.SystemPreferred)(MauiExternalLinkService.cs:22-27); everything else (mailto:,tel:,sms:) goes toLauncher.Default.TryOpenAsync(MauiExternalLinkService.cs:31), becauseBrowser.Defaultonly accepts http(s).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) makes
mailto:/tel:links work from inside the WebView where a plain anchor would silently do nothing. - Where it's used: registered for native heads; siblings are
BrowserExternalLinkServiceandNullExternalLinkService. Consumed by theExternalLinkcomponent.
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 viaDeviceInfo. - Depends on:
IFormFactor(inMMCA.Common.UI.Services); MAUI EssentialsDeviceInfo. - Concept: unlike the capability adapters above, this implements the older
IFormFactorcontract rather than aCapabilitiesinterface, but it follows the same per-host-adapter idea:[Rubric §22, Responsive/Cross-Browser]assesses whether the UI adapts across device classes, and this class supplies the native head's real idiom where the browser and WASM siblings supply a web-derived guess. - Walkthrough
GetFormFactor()(MauiFormFactor.cs:15): returnsDeviceInfo.Idiom.ToString()(Phone, Tablet, Desktop).GetPlatform()(MauiFormFactor.cs:18): returnsDeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString(e.g. Android, iOS, Windows, macOS with 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 heads share one implementation; the doc comment records its siblings
WebFormFactor(WebFormFactor) andWasmFormFactor(WasmFormFactor) and its registration entry pointAddMauiFormFactor(). - Where it's used: registered on native heads via
AddMauiFormFactor(); consumed by layout/responsive components that branch on form factor.
MauiHapticFeedbackService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiHapticFeedbackService.cs:11· Level 1 · class (sealed)
- What it is: the MAUI adapter for
IHapticFeedbackService, firing tactile feedback overHapticFeedback.DefaultandVibration.Default. - Depends on:
IHapticFeedbackService; MAUI EssentialsHapticFeedback/Vibration; BCLTimeSpan,OperatingSystem. - Concept: decoration-not-behavior capability (introduced by
IHapticFeedbackService).[Rubric §18, UI Architecture]: every failure is swallowed because a missing or blocked vibrator must never affect what the app does. - Walkthrough
IsSupported(MauiHapticFeedbackService.cs:14):!OperatingSystem.IsWindows(), since Windows has no haptics.Click()/LongPress()(MauiHapticFeedbackService.cs:17,:20): route to the privatePerformwith the matchingHapticFeedbackType.Vibrate(TimeSpan duration)(MauiHapticFeedbackService.cs:23): callsVibration.Default.Vibrate(duration), swallowing bothFeatureNotSupportedException(no motor/platform) andPermissionException(AndroidVIBRATEpermission missing from the manifest).Perform(HapticFeedbackType type)(MauiHapticFeedbackService.cs:39): callsHapticFeedback.Default.Perform, swallowing the same two exception types.
- Why it's built this way: synchronous
voidmethods match the fire-and-forget nature of a UI micro-cue (no caller waits on a buzz), and catching both the not-supported and permission-missing cases keeps a decorative effect strictly off the correctness path. - Where it's used: registered for native heads; the no-op fallback is
NullHapticFeedbackService. Consumed by interactive components (bookmark toggles, poll votes).
MauiLocalCacheStore
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiLocalCacheStore.cs:11· Level 1 · class (sealed)
- What it is: the MAUI adapter for
ILocalCacheStore, storing JSON documents as files in anmmca-cachefolder under the app data directory. - Depends on:
ILocalCacheStore; BCLSystem.IO.File/FileSystem,System.Text.Json. - Concept: last-known-good UI state for offline rendering (introduced by
ILocalCacheStore).[Rubric §29, Resilience & Business Continuity]: the on-device cache lets shared components render an offline snapshot when the API is unreachable. - Walkthrough
IsAvailable(MauiLocalCacheStore.cs:14):true, native heads always have a writable data directory.SetAsync<T>(...)(MauiLocalCacheStore.cs:17): resolves the path (creating the directory), JSON-serializes, andFile.WriteAllTextAsync, swallowingIOException/UnauthorizedAccessException, a failed write only means a colder next launch (MauiLocalCacheStore.cs:27-34).GetAsync<T>(...)(MauiLocalCacheStore.cs:38): returnsdefaultif the file is absent, else reads and deserializes, swallowingIOException/UnauthorizedAccessException/JsonExceptiontodefault(MauiLocalCacheStore.cs:53-64).RemoveAsync(...)(MauiLocalCacheStore.cs:68): deletes the file, swallowing IO failures.GetPath(string key, bool ensureDirectory)(MauiLocalCacheStore.cs:88): buildsmmca-cacheunderFileSystem.AppDataDirectoryand maps the key to a file name through a conservative character filter (letters, digits,-,.kept; everything else becomes_), appending.json. The doc comment notes keys are code-controlled, not user input (MauiLocalCacheStore.cs:8-9).
- Why it's built this way: file-per-key JSON keeps the store dependency-free (no embedded DB), and best-effort IO with
defaultreturns means a cache miss or corrupt file degrades to a live fetch rather than an error. - Where it's used: registered for native heads; siblings are
BrowserLocalCacheStoreand the unavailableNullLocalCacheStore. Consumed by offline-schedule components.
MauiMapNavigationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiMapNavigationService.cs:11· Level 1 · class (sealed)
- What it is: the MAUI adapter for
IMapNavigationService, launching the platform maps app for a street address viaLauncherURIs. - Depends on:
IMapNavigationService; MAUI EssentialsLauncher; BCLUri,OperatingSystem. - Concept: address-only navigation (introduced by
IMapNavigationService).[Rubric §18, UI Architecture]: address-based, not coordinate-based, because the domain model carries no geo-coordinates. - Walkthrough
OpenAddressAsync(string address, string? label, CancellationToken = default)(MauiMapNavigationService.cs:14): guards the address, URL-escapes it, builds the platform URI, and callsLauncher.Default.TryOpenAsync, returning its result orfalseonFeatureNotSupportedException(MauiMapNavigationService.cs:21-28). Thelabelparameter is currently unused by this adapter.BuildPlatformUri(string escapedQuery)(MauiMapNavigationService.cs:31): returnsgeo:0,0?q=...on Android,https://maps.apple.com/?q=...on iOS/MacCatalyst, andbingmaps:?q=...elsewhere. The method suppresses SonarAnalyzerS1075(MauiMapNavigationService.cs:35,:47) with a comment that these launcher URIs are the fixed per-platform maps integration point, not configurable paths.
- Why it's built this way: routing through the OS launcher (rather than an in-app map) needs no location permission and no geocoding round-trip; hard-coding the scheme per platform is correct because these are OS contracts, and the doc comment notes Android hosts must declare a
geointent in the manifest<queries>block. - Where it's used: registered for native heads; siblings are
BrowserMapNavigationServiceandNullMapNavigationService. Consumed by venue/location components.
MauiScreenshotService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiScreenshotService.cs:10· Level 1 · class (sealed)
- What it is: the MAUI adapter for
IScreenshotService, capturing the current screen to a temporary PNG viaScreenshot.Default. - Depends on:
IScreenshotService; MAUI EssentialsScreenshot/FileSystem; BCLSystem.IO,Guid. - Concept: permissionless temp-file capture (introduced by
IScreenshotService).[Rubric §30, Compliance/Privacy]and[Rubric §26, Front-End Security]: captures land in the cache directory, never the photo library, so no storage permission is prompted or held. - Walkthrough
IsSupported(MauiScreenshotService.cs:13):Screenshot.Default.IsCaptureSupported.CaptureToFileAsync(CancellationToken = default)(MauiScreenshotService.cs:16): returnsnullearly when capture is unsupported (:18-21); otherwise captures, writes toFileSystem.CacheDirectoryasmmca-screenshot-{guid:N}.png(:25-26), streams the PNG through nestedawait usingblocks that dispose the source and file streams deterministically (:28-36), and returns the path.FeatureNotSupportedExceptionandIOExceptionboth returnnull(:40-47).
- Why it's built this way: writing to the cache directory keeps the feature permission-free and lets the OS reclaim the files; the nullable path return lets the share flow abort quietly when capture is unsupported or fails.
- Where it's used: registered for native heads; the unsupported fallback is
NullScreenshotService. Its output is handed toIShareServicefor image sharing.
MauiShareService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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 overShare.Default. - Depends on:
IShareService; MAUI EssentialsShare/ShareTextRequest/ShareFileRequest; BCLUri. - Concept: share with a copy-link fallback (introduced by
IShareService).[Rubric §18, UI Architecture]: theboolreturns are what makeIClipboardServicecopy-link a viable fallback on heads without a share sheet. - Walkthrough
ShareLinkAsync(string title, Uri uri, CancellationToken = default)(MauiShareService.cs:9): null-guardsuri, requests aShareTextRequestcarryingTitleand the URI string (:15-19), returnstrue, orfalseonFeatureNotSupportedException.ShareFileAsync(string title, string filePath, string contentType, CancellationToken = default)(MauiShareService.cs:29): guardsfilePath, requests aShareFileRequestwrapping aShareFile(filePath, contentType)(:35-39), returnstrue, orfalseonFeatureNotSupportedException/IOException.
- Why it's built this way: presenting the OS share sheet reuses the platform's own target picker; the boolean returns let a Share button degrade to copy-link on heads where sharing is unavailable.
- Where it's used: registered for native heads; siblings are
BrowserShareServiceandNullShareService. ConsumesIScreenshotServiceoutput for image sharing andIClipboardServiceas its fallback.
MauiSpeechToTextService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/MauiSpeechToTextService.cs:14· Level 1 · class (sealed)
- What it is: the MAUI adapter for
ISpeechToTextService(ADR-042 Wave 4), driving CommunityToolkit.Maui'sSpeechToTextrecognizer and owning the microphone permission flow. - Depends on:
ISpeechToTextService;CommunityToolkit.Maui.Media.SpeechToText; BCLCultureInfo,IProgress<string>,TaskCompletionSource. - Concept: this adapter bridges an event-driven recognizer to the contract's single listen-until-final call.
[Rubric §21, Accessibility]and[Rubric §24, Forms/Validation/UX Safety]: dictation is an input affordance whose denial or 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): guardsculture; requests recognition permissions and returnsnullif denied (:29-32); creates aTaskCompletionSource<string?>; wiresOnUpdatedto forward interim text topartialResults(:36-37) andOnCompletedto resolve the finalTextwhen successful ornullotherwise (:39-40); subscribes both events, starts listening withSpeechToTextOptions(culture plusShouldReportPartialResultsonly when a progress sink was passed,:46-51); registers cancellation to resolvenull(:53-54); and in afinallyunsubscribes both handlers and callsStopListenAsync(:57-62).OperationCanceledExceptionand theInvalidOperationException/FeatureNotSupportedExceptionpair all returnnull(:64-71).
- Why it's built this way: the recognizer exposes start/stop with update/complete events, so a
TaskCompletionSourceis the idiomatic way to present it as one awaitable call; the guaranteed unsubscribe-and-stop infinallyprevents a leaked recognizer session across dictations. - Where it's used: registered for native heads; the fallback is
NullSpeechToTextService. Consumed by dictation affordances on text inputs.
MauiTextToSpeechService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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]and[Rubric §27, i18n]: read-aloud is an assistive output, and the adapter picks a voice for the current UI culture, falling back to the platform default so a device without anesvoice still speaks rather than throws. - Walkthrough
_gate(MauiTextToSpeechService.cs:14, aLock) and_activeUtterance(:15, a nullableCancellationTokenSource) track the one in-flight utterance.IsSupported(MauiTextToSpeechService.cs:18):true.SpeakAsync(string text, CancellationToken = default)(MauiTextToSpeechService.cs:21): guardstext, callsStopAsyncfirst so a new utterance preempts the previous (: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 and disposes the CTS (:46-56).StopAsync()(MauiTextToSpeechService.cs:60): reads the active CTS under the lock, returns if none, elseCancelAsync, swallowingObjectDisposedExceptionwhen the utterance completed concurrently (:73-80).Dispose()(MauiTextToSpeechService.cs:84): disposes and clears any active CTS under the lock.MatchLocaleAsync(CultureInfo culture)(MauiTextToSpeechService.cs:93): fetches installed locales and returns the first whoseLanguagematches the culture's two-letter code, ornull(the platform default) on no match orFeatureNotSupportedException.
- Why it's built this way: MAUI exposes no stop API, so
StopAsynccancels the in-flight utterance's token instead; theLock-guarded single-utterance state keeps overlappingSpeakAsynccalls from talking over each other; returningnullfrom locale matching lets the platform choose a voice rather than failing. - Where it's used: registered for native heads; the fallback is
NullTextToSpeechService. Consumed by read-aloud affordances.
MauiExternalAuthBroker
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common.UI.Maui/Capabilities/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<ApiSettings>(ApiSettingsinMMCA.Common.UI.Common.Settings),IConfiguration; MAUI EssentialsWebAuthenticator. This is the only Level 2 type in the unit because it composes over app configuration and navigation rather than a single platform static. - Concept: native OAuth callback capture (introduced by
IExternalAuthBroker).[Rubric §11, Security]and[Rubric §26, Front-End Security]: identity providers reject embedded WebViews, so the flow runs in the system browser and a single-use code (never tokens) returns over a custom scheme; the shared completion page owns the exchange and token storage, keeping the sensitive step in exactly one place (ADR-043). - Walkthrough
- The constructor (
MauiExternalAuthBroker.cs:26) null-guardsconfiguration, storesNavigationManagerandIOptions<ApiSettings>, and reads the callback scheme fromconfiguration["OAuth:MobileRedirectScheme"](:35). IsAvailable(MauiExternalAuthBroker.cs:39):trueonly when the callback scheme is configured, so an unconfigured head keeps the web anchor flow.SignInAsync(string provider, CancellationToken = default)(MauiExternalAuthBroker.cs:42): guardsprovider; returnsfalsewhen unavailable (:46-49) or when the API base URL is missing (:51-55); builds{scheme}://oauth-completeas the callback and{apiBase}/auth/oauth/{provider}?returnUrl=...as the authorize URL (:57-59); callsWebAuthenticator.Default.AuthenticateAsyncwith those URLs (:63-69); returnsfalseif nocodeproperty 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).
- The constructor (
- 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 code-to-token exchange to the existing
/auth/oauth-completepage means the single-use-code contract, token storage, and auth-state refresh live in one place across all heads (ADR-043). - Where it's used: registered for native heads; the fallback is
UnavailableExternalAuthBroker. 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.
MauiGeocodingService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiGeocodingService.cs:10· Level 2 · class
- What it is: the MAUI-native implementation of
IGeocodingService: it turns a free-text address into a latitude/longitudeGeoPointby delegating to MAUI Essentials'Geocoding.Default. It is the concrete leg selected on the mobile/desktop head; the browser and inert fallbacks live elsewhere in this group. - Depends on:
IGeocodingServiceandGeoPoint(the capability contract and its value carrier, both defined inMMCA.Common.UI,MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiGeocodingService.cs:1); MAUI EssentialsGeocoding.DefaultandFeatureNotSupportedException(NuGet,Microsoft.Maui.Essentials). - Concept introduced: the per-host capability adapter pattern is introduced by the contracts and browser adapters earlier in this group, so it is only cross-referenced here: one interface in
MMCA.Common.UI, several implementations (MAUI-native, browser-JS, inert null), and DI at the host picks exactly one (ADR-042). This file also shows the group's signature degrade-to-null discipline: every proximity affordance returnsnullrather than throwing when the platform cannot answer.[Rubric §22 - Responsive/Cross-Browser]assesses how gracefully the app spans device classes and runtimes; this adapter embodies it by giving the mobile head a real geocoder while the same call site degrades cleanly where geocoding is absent.[Rubric §10 - Cross-Cutting]assesses how uniformly ambient concerns are handled; the swallow-and-return-null contract is applied identically across every capability here. - Walkthrough:
IsSupportedis a constanttrue(MauiGeocodingService.cs:13): on a MAUI head geocoding is always wired, since it is a network lookup rather than a permission-gated sensor.GeocodeAsync(MauiGeocodingService.cs:16) guards the input withArgumentException.ThrowIfNullOrWhiteSpace(address)(:18), then callsGeocoding.Default.GetLocationsAsync(address)and takesFirstOrDefault()of the returned candidates (:22-23). A hit becomesnew GeoPoint(first.Latitude, first.Longitude); an empty result staysnull(:24). Two catch arms enforce the degrade contract:FeatureNotSupportedException(:26) covers a platform with no geocoder, and a filtered catch forTimeoutException,InvalidOperationException, orIOException(:30) covers an offline or unavailable geocoder. Both simply returnnull, so the proximity hint is omitted rather than surfaced as an error. - Why it's built this way: geocoding is a non-essential enhancement (a proximity hint on a card), so the correct failure mode is silence, not an exception the UI must handle. Note the deliberate narrow exception filter at
:30: it does not blanket-catch, so a programming error outside those known-transient types still propagates. The adapter split (native here, browser/null elsewhere) is the ADR-042 device-capability-abstraction decision. - Where it's used: resolved wherever the app requests
IGeocodingService; registered on the MAUI head by that package's capability wiring (theUseMauiDeviceCapabilitiescomposition described in ADR-042), overriding theTryAddfallback thatAddUISharedregisters.
MauiGeolocationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiGeolocationService.cs:11· Level 2 · class
- What it is: the MAUI-native
IGeolocationService: it reads the device's current position (or a recent cached fix) and returns aGeoPoint, driving the soft when-in-use location-permission flow itself. - Depends on:
IGeolocationServiceandGeoPoint(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiGeolocationService.cs:1); MAUI EssentialsGeolocation.Default,Permissions.LocationWhenInUse,PermissionStatus,GeolocationRequest,MainThread, and theFeatureNotSupportedException/FeatureNotEnabledException/PermissionExceptionfamily (NuGet,Microsoft.Maui.Essentials); BCLTimeSpan/DateTimeOffset. - Concept introduced: this is the first capability in the unit that touches an OS permission prompt, so it teaches the soft-permission pattern: check the current grant, request it once on the main thread only if not already granted, and treat any denial as a
nullresult rather than a blocking error. Unlike geocoding (a network call), reading location is a sensor read the OS gates.[Rubric §26 - Front-End Security]assesses least-privilege access to device-sensitive capabilities; requesting when-in-use (not always-on) location and prompting at most once embodies that restraint.[Rubric §11 - Security](the general lens) applies for the same reason: the code never assumes access it has not been granted. - Walkthrough: two
static readonlytunables set policy:LastKnownFreshness = TimeSpan.FromMinutes(5)andCurrentFixTimeout = TimeSpan.FromSeconds(10)(MauiGeolocationService.cs:13-14).IsSupportedistrue(:17).GetCurrentOrLastKnownAsync(:20) first checksPermissions.CheckStatusAsync<Permissions.LocationWhenInUse>()(:24); if notGrantedit requests once, marshalled throughMainThread.InvokeOnMainThreadAsyncwith astaticlambda so the platform prompt runs on the UI thread (:27-28). Still not granted returnsnull(:31-34). It then prefers a cheap cached fix:Geolocation.Default.GetLastKnownLocationAsync()is accepted only ifIsFresh(:36-40), whereIsFreshcompares the fix timestamp againstUtcNow - LastKnownFreshness(:61-62). Otherwise it takes a live fix viaGetLocationAsyncwith aGeolocationRequest(GeolocationAccuracy.Medium, CurrentFixTimeout), passing the caller'scancellationToken(:42-43). Three catch arms returnnull:FeatureNotSupportedException(no GPS),FeatureNotEnabledException(location services off at the OS), andPermissionException(:46-58). - Why it's built this way: the last-known-fresh-else-live ladder trades precision for battery and latency: a five-minute-old fix is good enough for a proximity hint and avoids waking the GPS.
GeolocationAccuracy.Mediumand a ten-second timeout keep the read cheap. Running the request on the main thread is required by the platform prompt APIs. The whole thing degrades tonullfor the same reason geocoding does: location is an enhancement, not a hard dependency (ADR-042). - Where it's used: resolved via
IGeolocationServiceby any feature wanting device proximity; registered on the MAUI head, overriding the sharedTryAddfallback.
MauiLocalNotificationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiLocalNotificationService.cs:13· Level 2 · class
- What it is: the MAUI-native
ILocalNotificationService: it schedules, cancels, and requests permission for local (on-device, no server) notifications by mapping the framework'sLocalNotificationRequestonto the Plugin.LocalNotification package. - Depends on:
ILocalNotificationService,LocalNotificationRequest, andIDeepLinkDispatcher(referenced in the doc contract for tap routing,MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiLocalNotificationService.cs:11); thePlugin.LocalNotificationNuGet package (LocalNotificationCenter,NotificationRequest,NotificationRequestSchedule,:2-3); BCLDateTimeOffset. - Concept introduced: this is the group's anti-corruption mapping example: the framework's own
LocalNotificationRequestDTO is kept independent of the third-partyNotificationRequestshape, and this adapter is the single place the two are reconciled. Callers never see the plugin type.[Rubric §2 - Design Patterns]assesses disciplined use of patterns; the adapter/anti-corruption boundary here keeps a swappable dependency from leaking into feature code.[Rubric §26 - Front-End Security]again applies via the explicit permission request that respects Android 13+POST_NOTIFICATIONSand iOS authorization. - Walkthrough:
IsSupportedistrue(MauiLocalNotificationService.cs:16).RequestPermissionAsync(:19) short-circuits totrueifAreNotificationsEnabled()already reports granted, else callsRequestNotificationPermission(); anInvalidOperationExceptiondegrades tofalse(:23-33).ScheduleAsync(:37) null-guards the request (:39), silently drops anyDeliverAtat or before now (:41-44, no past-dated reminders), then maps field-by-field into a pluginNotificationRequest:Id → NotificationId,Title,Body → Description,DeepLinkRoute ?? string.Empty → ReturningData, andDeliverAt → Schedule.NotifyTime(:46-56). TheShowcall is wrapped so a mid-session permission revocation (InvalidOperationException) becomes a no-op reminder rather than a crash (:58-65).CancelAsync(:69) null-guards the id list and, only when non-empty, callsCancel([.. ids])(a collection expression spreading the ids into the plugin's params array,:73-75);CancelAllAsync(:82) forwards toCancelAll(). Both cancel methods returnTask.CompletedTask(the plugin calls are synchronous). - Why it's built this way: the doc comment records two deliberate platform choices (
:8-11): scheduling uses inexact platform alarms (noSCHEDULE_EXACT_ALARM, which Play policy restricts), and notification taps are routed toIDeepLinkDispatcherby the package bootstrap, not by this service, keeping tap-handling in the single deep-link route table (ADR-042). Carrying the deep-link route throughReturningDatais how a tapped reminder later reaches the dispatcher. - Where it's used: resolved via
ILocalNotificationServiceby features scheduling reminders; registered on the MAUI head over the shared fallback. - Caveats / not-in-source: the actual tap-to-dispatcher wiring is asserted by the doc comment to live in the package bootstrap; it is not in this file. Not determinable from source here: the exact bootstrap that binds
ReturningDataback toIDeepLinkDispatcher.
MauiMediaPickerService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiMediaPickerService.cs:11· Level 2 · class
- What it is: the MAUI-native
IMediaPickerService: it lets the user pick a photo from the library or capture one with the camera, returning aPickedMedia(an open stream plus filename and content type). It backs the avatar-upload flow (ADR-045). - Depends on:
IMediaPickerServiceandPickedMedia(MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiMediaPickerService.cs:1); MAUI EssentialsMediaPicker.Default,DeviceInfo.Current,DevicePlatform, andFileResult(NuGet,Microsoft.Maui.Essentials). - Concept introduced: reinforces the degrade-to-null discipline for a user-cancellable, permission-gated capability, with one nuance the earlier adapters lack: it distinguishes cancellation from failure. An
OperationCanceledExceptionis rethrown so the caller's own token semantics are honored, while every other failure (denied permission, unsupported device) collapses tonull.[Rubric §24 - Forms/Validation/UX Safety]assesses how safely user-input flows handle abandonment and error; preserving cancellation while swallowing capability failures is exactly that safety.[Rubric §22 - Responsive/Cross-Browser]applies through the platform-awareIsSupported. - Walkthrough:
IsSupported(MauiMediaPickerService.cs:14) istruewhen either capture is supported or the platform is not WinUI, so the library-pick path stays available on desktop even where the camera is not.PickPhotoAsync(:17) delegates toMediaPicker.Default.PickPhotoAsync()through the sharedPickCoreAsynchelper, with a scoped#pragma warning disable CS0618(:18-20): the single-select API is flagged obsolete in favor of multi-select, but an avatar is exactly one photo, so the suppression is intentional and documented inline.CapturePhotoAsync(:23) checksIsCaptureSupportedfirst and returnsTask.FromResult<PickedMedia?>(null)when there is no camera, otherwise routes through the same helper.PickCoreAsync(:28) awaits the platform pick, returnsnullon a cancelled sheet (anullFileResult,:33-35), opens the file stream, honorscancellationToken.ThrowIfCancellationRequested()after the read starts (:38-39), and wraps the result asnew PickedMedia(stream, file.FileName, file.ContentType ?? "application/octet-stream")(:40). The catch block rethrowsOperationCanceledException(:42-45) but blanket-catches everything else tonullunder a scoped#pragma warning disable CA1031(:46-51), since a denied permission is a normal outcome here, not an error. - Why it's built this way: an avatar picker must never crash the page: a user who declines the permission or backs out of the sheet should land back on the form unchanged, which is why the only exception allowed to escape is cancellation. The returned stream is left open for the caller (upload/processing) to consume and dispose. Passing the picked bytes to the server-side avatar contract (2 MB in, 256x256 JPEG out, all metadata stripped) is the ADR-045 pipeline this feeds.
- Where it's used: resolved via
IMediaPickerServiceby the avatar-upload UI; on web heads the same contract is satisfied by anInputFile-based implementation instead (ADR-045), registered on the MAUI head over the shared fallback.
MauiPushRegistrationService
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui.Capabilities·MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiPushRegistrationService.cs:15· Level 2 · class
- What it is: the native push-registration orchestrator (ADR-044): it fetches the platform push token, maintains a stable client-generated installation id in device preferences, and syncs (or removes) the installation on the server's
Notifications/Devicesendpoints. It is the most collaborator-heavy adapter in this unit and the only one taking constructor dependencies. - Depends on:
IPushRegistrationService(the contract it implements),IPushDeviceTokenProvider(the app-supplied token source), andIDevicePreferences(persistent key/value store), all injected via primary constructor (MMCA.Common/Source/Presentation/MMCA.Common.UI.Maui/Capabilities/MauiPushRegistrationService.cs:15-19); BCLIHttpClientFactory,PutAsJsonAsync/DeleteAsync(System.Net.Http.Json,:1),ILogger<T>with source-generated[LoggerMessage](:2), andGuid. - Concept introduced: this is the unit's first adapter that talks to the backend rather than only the OS, and it introduces the group's inert-by-construction default: the service is fully wired but does nothing useful until the app supplies a credentialed
IPushDeviceTokenProvider, because the default provider yields no token andRegisterAsyncreturns early. It also uses source-generated logging ([LoggerMessage]partial methods) rather than string-interpolated log calls.[Rubric §13 - Observability]assesses how well operational events are captured; the three[LoggerMessage]methods give push registration structured, allocation-light diagnostics without breaking the never-throw contract.[Rubric §9 - API & Contract Design]applies through the REST sync (PUT to register, DELETE to unregister) against theNotifications/Devicescontract.[Rubric §26 - Front-End Security]applies because the sync rides the authenticated"APIClient". - Walkthrough: the class is
sealed partial(partial for the generated log methods) with a primary constructor capturing the four dependencies (MauiPushRegistrationService.cs:15-19);InstallationIdKeyis a constant preferences key (:21),IsSupportedistrue(:24).RegisterAsync(:27) askstokenProvider.GetTokenAsync; anulltoken returnsfalse(the inert path,:31-35). Otherwise it gets-or-creates the installation id, creates the named"APIClient"HTTP client (:38, the authenticated pipeline), andPutAsJsonAsyncto the relativeNotifications/Devicesuri a body of{ InstallationId, token.Platform, PushChannel = token.Token }(:39-42). A non-success status logsLogRegistrationRejectedwith the status code and returnsfalse(:44-48); success returnstrue.UnregisterAsync(:62) reads the stored installation id, no-ops if absent (:66-70), andDeleteAsynctoNotifications/Devices/{escaped-id}withUri.EscapeDataStringguarding the path segment (:72-75).GetOrCreateInstallationIdAsync(:85) returns the persisted id if present, else generatesGuid.NewGuid().ToString("N"), persists it viadevicePreferences.SetAsync, and returns it (:87-95) so the same physical device keeps one stable identity across launches. Both public methods blanket-catch (scoped#pragma warning disable CA1031,:52-58and:77-82) into a warning log and a benign return, honoring the never-throw contract. The three[LoggerMessage]partials (:98-105) declare the rejected/failed/unregister-failed events atWarning. - Why it's built this way: registration is a best-effort side channel: the inbox stays the source of truth (ADR-044), so a failed device sync must never surface to the user or throw. A client-generated stable installation id lets the server upsert one row per device (PUT is idempotent) and delete it precisely on sign-out, without the server minting ids. The Null token provider default keeps credential-less builds compiling and wired but inert (
:12-13), so a downstream app opts in to push simply by registering a real provider.Notifications/DevicesPUT/DELETE is the ADR-044DevicesControllercontract. - Where it's used: resolved via
IPushRegistrationServiceon the MAUI head; invoked around sign-in (register) and sign-out (unregister). The server side is the ADR-044 native-push pipeline (INativePushSender/IPushDeviceRegistrar). - Caveats / not-in-source: the concrete
"APIClient"handler chain (base address, bearer-token attachment) is configured by the head's HTTP wiring, not in this file. Not determinable from source here: whichIPushDeviceTokenProviderimplementation a given app registers (the default is the inert Null provider per ADR-044).
AlwaysOnlineConnectivityStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/AlwaysOnlineConnectivityStatusService.cs:7· Level 1 · class
- What it is: the default, host-agnostic implementation of
IConnectivityStatusService: it reports the app as permanently online and never signals a connectivity change. - Depends on:
IConnectivityStatusService(the contract it satisfies) and BCLEventHandler/ValueTaskonly. No native platform APIs, noMMCA.Common.UI.Mauidependency. - Concept introduced (the inert-fallback / Null Object tier of the device-capability layer). This is the first fallback type in the unit, so the pattern is worth teaching from first principles. Every device capability the UI can use (biometrics, geolocation, speech, haptics, sharing, push registration, connectivity, battery, and so on) is expressed as a small per-capability interface in
MMCA.Common.UI.Services.Capabilities. Because a single shared Blazor component tree runs on three very different hosts (a MAUI Blazor Hybrid native shell, a browser Blazor WebAssembly/Server head, and backend-less test/gallery hosts), a component cannot assume any given capability exists. The framework resolves this with three registration tiers, selected per host at DI composition time (ADR-042): the MAUI package registers real native adapters, the browser head registers JS-interop adapters, andAddUISharedregisters this fallback tier withTryAddso that some implementation is always resolvable even when nothing richer was added. The fallbacks are pure Null Objects: they satisfy the interface, do the safe inert thing, and never throw, so calling code never has to null-check a capability. This is [Rubric §2, Design Patterns] (the Null Object pattern applied uniformly across a capability surface) and [Rubric §1, SOLID] (Liskov substitutability: a component holding the interface behaves correctly whichever tier answered, and the dependency-inversion boundary keeps components off concrete platform APIs). It is also [Rubric §18, UI Architecture] and [Rubric §22, Responsive / Cross-Browser], because the same component set degrades gracefully from a native shell down to a plain web page without host-specific branches in the markup. - Walkthrough: the class is
sealed(AlwaysOnlineConnectivityStatusService.cs:7). TheConnectivityChangedevent (:10) declares explicitadd/removeaccessors whose bodies are intentionally empty (:12,:17): subscribers are accepted but the event is never raised, because on this host connectivity is treated as constant.IsOnline(:24) is an expression-bodied property returningtrue.InitializeAsync(:27) returnsValueTask.CompletedTask, so warm-up is a no-op. - Why it's built this way: the class comment (
:4) records the rationale: on Blazor Server a dropped connection tears down the SignalR circuit itself, so a per-app "am I online?" signal is meaningless there, and "always online, never raises" is the correct default rather than a mere stub. Native heads that genuinely track radio state override it withAdd. - Where it's used: resolved by any component or service that injects
IConnectivityStatusServiceon a host that did not register a native/browser connectivity adapter.
InMemoryDevicePreferences
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/InMemoryDevicePreferences.cs:10· Level 1 · class
- What it is: the default
IDevicePreferences: a non-persistent key/value store that keeps preference values in memory for the lifetime of its DI scope only. - Depends on:
IDevicePreferencesand BCLConcurrentDictionary<string, object?>. - Concept introduced: unlike the pure Null Objects around it, this fallback carries real behavior: it is the only member of this unit that actually stores and returns data. It signals its limitation through the interface contract rather than by failing, which is a recurring shape in this layer (a
bool"is this real?" property that hosts read to decide whether to show capability-dependent UI). See the fallback-tier concept underAlwaysOnlineConnectivityStatusService. [Rubric §19, State Management] applies: preferences are the small slice of client state whose durability differs per host, and the durability answer is data (IsPersistent), not a thrown exception. - Walkthrough: a
readonly ConcurrentDictionary<string, object?>built withStringComparer.Ordinalbacks the store (:12); the concurrent type is chosen because Blazor scopes can be touched from more than one async continuation.IsPersistent(:15) returnsfalse, the honest admission that these values do not survive a restart.GetAsync<T>(:18) guards the key withArgumentException.ThrowIfNullOrWhiteSpace, then returns the stored value only when it is present and of the requested typeT, otherwise the caller-suppliedfallback(:22).SetAsync<T>(:28) andRemoveAsync(:37) apply the same key guard, then write orTryRemove. All three return completedTasks (the work is synchronous under an async contract). - Why it's built this way: the class comment (
:5) explains the intended consumer behavior: becauseIsPersistentisfalse, a host can hide device-settings UI that would otherwise promise durability it cannot deliver. A native head swaps in a platform-backed preferences store withIsPersistent => true. - Where it's used: injected wherever the UI reads or writes device-scoped preferences and no persistent adapter was registered.
NullAccessibilityAnnouncer
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullAccessibilityAnnouncer.cs:4· Level 1 · class
- What it is: the default
IAccessibilityAnnouncer: screen-reader announcements are silently dropped. - Depends on:
IAccessibilityAnnouncerand BCLTaskonly. - Walkthrough: a single
sealedclass (:4) withAnnounceAsync(string message, ...)returningTask.CompletedTask(:7). No state, no side effects. See the fallback-tier concept underAlwaysOnlineConnectivityStatusService. - Why it's built this way: a Null Object keeps [Rubric §21, Accessibility] wiring resolvable on every host; heads that own a live-region or native accessibility API replace it so announcements actually reach assistive technology.
- Where it's used: injected by components that push polite/assertive live-region messages.
NullBatteryStatusService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullBatteryStatusService.cs:4· Level 1 · class
- What it is: the default
IBatteryStatusService: energy-saver mode is never reported active and the change event never fires. - Depends on:
IBatteryStatusServiceand BCLEventHandler. - Walkthrough: the
sealedclass (:4) mirrors the connectivity fallback's shape:EnergySaverChanged(:7) has emptyadd/removeaccessors (:9,:14) so subscriptions are accepted but never invoked, andIsEnergySaverOn(:21) returnsfalse. SeeAlwaysOnlineConnectivityStatusServicefor the event-never-raised idiom and the fallback tier. - Why it's built this way: web hosts have no OS battery signal, so "never energy-saving" is the safe default that lets performance-sensitive components (for example, animation throttling) run unthrottled; native heads override with real battery telemetry.
- Where it's used: injected by components that adapt behavior to low-power mode.
NullBiometricAuthenticator
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullBiometricAuthenticator.cs:4· Level 1 · class
- What it is: the default
IBiometricAuthenticator: biometric app-lock is reported unavailable and any authentication attempt fails. - Depends on:
IBiometricAuthenticatorand BCLTask<bool>. - Walkthrough: the
sealedclass (:4) returnsTask.FromResult(false)from bothIsAvailableAsync(:7) andAuthenticateAsync(string reason, ...)(:11). The availability flag and the auth result are the samefalse, so a host that checks availability first will never call authenticate. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:3) notes hosts hide the app-lock toggle when biometrics are unavailable, which keeps the security affordance ([Rubric §26, Front-End Security]) honest: no fake lock UI on a head that cannot enforce it. MAUI supplies a real fingerprint/face adapter. - Where it's used: injected by app-lock / sensitive-screen guard components.
NullClipboardService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullClipboardService.cs:4· Level 1 · class
- What it is: the default
IClipboardService: copying text to the clipboard is reported as failed. - Depends on:
IClipboardServiceand BCLTask<bool>. - Walkthrough: the
sealedclass (:4) has one member,SetTextAsync(string text, ...), returningTask.FromResult(false)(:7). Theboolreturn is the failure signal callers branch on. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: returning
falserather than throwing lets a copy-to-clipboard button degrade to an alternate UX (for example, showing selectable text) instead of erroring; browser heads register a JS-interop clipboard adapter that returnstrue. - Where it's used: injected by copy-link / copy-code UI elements.
NullExternalLinkService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullExternalLinkService.cs:7· Level 1 · class
- What it is: the default
IExternalLinkService: it does not intercept outbound links, so components render ordinary anchors. - Depends on:
IExternalLinkServiceand BCLUri/Task. - Walkthrough: the
sealedclass (:7) exposesInterceptsLinksreturningfalse(:10) andOpenAsync(Uri uri, ...)returningTask.CompletedTask(:13). BecauseInterceptsLinksisfalse, well-behaved components never callOpenAsyncat all; they emit a plain<a target="_blank">. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:3) explains this is the correct behavior on web heads even without JS: a normal anchor already opens an external URL in a new tab. It exists as an override point for BlazorWebView (MAUI), wheretarget="_blank"is dead and links must be handed to the OS browser instead (ADR-042). This is theExternalLink-over-raw-anchor decision recorded in ADR-042. - Where it's used: injected by the shared
ExternalLinkcomponent and anywhere the UI opens off-app URLs.
NullHapticFeedbackService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullHapticFeedbackService.cs:4· Level 1 · class
- What it is: the default
IHapticFeedbackService: no vibration hardware, so every haptic call is a no-op. - Depends on:
IHapticFeedbackServiceand BCLTimeSpan. - Walkthrough: the
sealedclass (:4) reportsIsSupported => false(:7) and provides emptyClick()(:10),LongPress()(:16), andVibrate(TimeSpan duration)(:22). These are the unit's only synchronous (non-Task) capability methods, matching the fire-and-forget nature of haptics. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: no-op methods plus an
IsSupportedflag let interaction code request haptics unconditionally while a host with a vibrator motor opts in; the fallback keeps web and desktop heads quiet. - Where it's used: injected by tactile-feedback wrappers around buttons and gestures.
NullLocalCacheStore
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullLocalCacheStore.cs:4· Level 1 · class
- What it is: the default
ILocalCacheStore: nothing is cached, and reads always returndefault. - Depends on:
ILocalCacheStoreand BCLTask/Task<T?>. - Walkthrough: the
sealedclass (:4) reportsIsAvailable => false(:7).SetAsync<T>(:10) andRemoveAsync(:18) returnTask.CompletedTask(writes are discarded);GetAsync<T>(:14) returnsTask.FromResult<T?>(default), so a miss is guaranteed. This is a write-black-hole / read-empty store. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way:
IsAvailablelets a component skip an offline read path entirely when no durable local cache exists; native/browser heads supply a real store (for example, over platform storage or IndexedDB). [Rubric §23, Front-End Performance] is the relevant lens: local caching is a performance capability the fallback declares absent rather than faking. - Where it's used: injected by offline-read and response-caching UI helpers.
NullMapNavigationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullMapNavigationService.cs:4· Level 1 · class
- What it is: the default
IMapNavigationService: no maps integration; opening an address reports failure. - Depends on:
IMapNavigationServiceand BCLTask<bool>. - Walkthrough: the
sealedclass (:4) has one member,OpenAddressAsync(string address, string? label, ...), returningTask.FromResult(false)(:7). The nullablelabelis accepted and ignored. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: a
falseresult lets an "open in maps" affordance fall back to plain address text; native heads launch the OS maps app. - Where it's used: injected by venue / location UI that offers turn-by-turn hand-off.
NullPushRegistrationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullPushRegistrationService.cs:7· Level 1 · class
- What it is: the default
IPushRegistrationService: OS-level push registration is unsupported, so registering is a failed no-op. - Depends on:
IPushRegistrationServiceand BCLTask/Task<bool>. - Walkthrough: the
sealedclass (:7) reportsIsSupported => false(:10),RegisterAsyncreturnsTask.FromResult(false)(:13), andUnregisterAsyncreturnsTask.CompletedTask(:16) so tearing down is always safe to call. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:3) records the ADR-044 rationale precisely: web heads already receive real-time notifications over the SignalR hub while the page is open and have no OS-level installation to manage, so no-op push registration is correct, not a gap. Native heads register with Azure Notification Hubs (FCM v1 / APNs) via the MAUI package. This is the client leg of ADR-044's native push channel; the credential-less-but-inert posture keeps builds wired without a device token. - Where it's used: injected by notification-preferences / permission-prompt UI.
NullScreenshotService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullScreenshotService.cs:4· Level 1 · class
- What it is: the default
IScreenshotService: screen capture is unavailable. - Depends on:
IScreenshotServiceand BCLTask<string?>. - Walkthrough: the
sealedclass (:4) reportsIsSupported => false(:7) andCaptureToFileAsyncreturningTask.FromResult<string?>(null)(:10) (a null file path meaning "no capture"). See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the null-path convention lets a "save screenshot" action gate itself on
IsSupportedand skip cleanly; native heads implement real capture-to-file. - Where it's used: injected by share-a-screenshot / diagnostics UI.
NullShareService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullShareService.cs:4· Level 1 · class
- What it is: the default
IShareService: the native share sheet is unavailable, so callers fall back to copy-link. - Depends on:
IShareServiceand BCLUri/Task<bool>. - Walkthrough: the
sealedclass (:4) returnsTask.FromResult(false)from bothShareLinkAsync(string title, Uri uri, ...)(:7) andShareFileAsync(string title, string filePath, string contentType, ...)(:11). See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:3) names the intended degradation: afalseresult routes callers to copy-link instead of a native share sheet. MAUI supplies a real share adapter. - Where it's used: injected by share buttons on shareable entities (sessions, links, files).
NullSpeechToTextService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullSpeechToTextService.cs:6· Level 1 · class
- What it is: the default
ISpeechToTextService: no recognizer, so dictation is unavailable. - Depends on:
ISpeechToTextService, BCLCultureInfo,IProgress<string>,Task<string?>. - Walkthrough: the
sealedclass (:6) reportsIsSupported => false(:9).ListenAsync(CultureInfo culture, IProgress<string>? partialResults, ...)(:12) accepts a target culture and an optional partial-results reporter but returnsTask.FromResult<string?>(null)(:16) immediately: no interim progress is ever reported and the final result is null. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:5) notes components hide the microphone affordance whenIsSupportedis false; native heads provide a platform speech recognizer that streams partials through theIProgress<string>. - Where it's used: injected by voice-input / dictation controls.
NullTextToSpeechService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/NullTextToSpeechService.cs:4· Level 1 · class
- What it is: the default
ITextToSpeechService: no synthesizer, so speaking text is a no-op. - Depends on:
ITextToSpeechServiceand BCLTask. - Walkthrough: the
sealedclass (:4) reportsIsSupported => false(:7);SpeakAsync(string text, ...)(:10) andStopAsync()(:13) both return completedTasks. The paired speak/stop shape means a caller can always cancel safely even when nothing is speaking. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:3) notes components hide the read-aloud affordance when unsupported; native heads provide a platform synthesizer. - Where it's used: injected by read-aloud / accessibility narration controls.
UnavailableExternalAuthBroker
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common.UI/Services/Capabilities/Fallbacks/UnavailableExternalAuthBroker.cs:7· Level 1 · class
- What it is: the default
IExternalAuthBroker: there is no native OAuth broker, so external sign-in falls through to the web flow. - Depends on:
IExternalAuthBrokerand BCLTask<bool>. - Walkthrough: the
sealedclass (:7) reportsIsAvailable => false(:10) andSignInAsync(string provider, ...)returningTask.FromResult(false)(:13). BecauseIsAvailableisfalse, the shared Login page never invokes the broker. See the fallback tier underAlwaysOnlineConnectivityStatusService. - Why it's built this way: the class comment (
:3) explains this keeps the shared Login page on its anchor-href OAuth flow, the correct behavior for web heads. On MAUI theMauiExternalAuthBrokeroverrides it to driveWebAuthenticatorand capture the single-use completion code over the allow-listed custom scheme (ADR-043). This is [Rubric §11, Security]: the native-vs-web sign-in path is chosen behind one interface, and the fallback picks the browser-safe OAuth redirect by default. - Where it's used: injected by the shared Identity Login page's external-provider buttons.
NullGeocodingService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Fallbacks/NullGeocodingService.cs:4· Level 2 · class
- 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. - 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 teach the shape once here. Every device capability in this group is an interface (biometrics, geolocation, media picking, push, and so on), and shared Blazor components resolve those interfaces directly. 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 (the Null Object pattern: a real, substitutable instance whose methods do nothing observable rather than a
nullreference). The two moving parts here are theIsSupportedflag (a component reads it and hides the affordance) and the operation itself (a component that ignoresIsSupportedand calls anyway still gets a safe,nullanswer, never an exception).AddDeviceCapabilityDefaultsTryAdd-registers this class as a singleton (MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/DependencyInjection.cs:33), 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.- [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 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 distance hint entirely.GeocodeAsync(NullGeocodingService.cs:10) ignores itsaddressandcancellationTokenarguments and returnsTask.FromResult<GeoPoint?>(null), an already-completed task, so there is no allocation of a real async state machine and no thread hop. - Why it's built this way - ADR-042 (device capability abstraction). The domain model deliberately stores addresses, not coordinates, so geocoding is a pure presentation-time convenience; making the unsupported case a first-class
null(rather than an exception or a feature flag the caller must check) keeps proximity hints optional everywhere. - Where it's used - registered by
AddDeviceCapabilityDefaults(DependencyInjection.cs:33) and resolved by any component that shows a "distance from venue" hint. The MAUI head replaces it withMauiGeocodingService; there is no browser override, so web heads keep this null default.
NullGeolocationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Fallbacks/NullGeolocationService.cs:4· Level 2 · class
- 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. - 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).GetCurrentOrLastKnownAsync(NullGeolocationService.cs:10) returnsTask.FromResult<GeoPoint?>(null); because it never touches the platform it also never fires the OS permission prompt that the real contract warns about, which is the desired behavior on a head that cannot honor it. - Why it's built this way - ADR-042. Location is opt-in and best-effort by contract (permission denial and timeout also yield
null), so a head with no location provider is just the permanent version of that same "no fix" outcome. - Where it's used - registered by
AddDeviceCapabilityDefaults(DependencyInjection.cs:32). The MAUI head overrides it withMauiGeolocationService; web heads keep this default (there is no browser geolocation implementation inAddBrowserDeviceCapabilities).
NullLocalNotificationService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Fallbacks/NullLocalNotificationService.cs:4· Level 2 · class
- What it is - the inert default for
ILocalNotificationService: on-device notification scheduling is unavailable. It denies permission and swallows every schedule/cancel call, and hosts readIsSupportedto hide the reminder-settings UI entirely. - 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 (no browser equivalent that the framework wires), so the default has to make both the capability probe and the actions safe:
IsSupportedfalse steers 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), reporting permission as not granted so callers never attempt to schedule.ScheduleAsync(NullLocalNotificationService.cs:14),CancelAsync(NullLocalNotificationService.cs:18), andCancelAllAsync(NullLocalNotificationService.cs:22) each returnTask.CompletedTask, doing nothing with their arguments. - Why it's built this way - ADR-042. The real contract already specifies that scheduling without permission is a no-op, so the null default is that rule taken to its limit (permission is never granted, therefore nothing is ever scheduled), which keeps reminder features degradable to nothing on web without any conditional code in the feature.
- Where it's used - registered by
AddDeviceCapabilityDefaults(DependencyInjection.cs:37); overridden byMauiLocalNotificationServiceon native heads. Web and Server heads keep this default.
NullMediaPickerService
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Fallbacks/NullMediaPickerService.cs:7· Level 2 · class
- What it is - the no-op default for
IMediaPickerService(avatar photo pick/capture). It reports the native picker unavailable and returnsnullfrom both operations, which for web heads means "render a plainInputFileinstead," not a degraded experience. - Depends on - implements
IMediaPickerService; returnsPickedMedia?. Same null-object shape as the siblings above. - Concept introduced - the "affordance switch, not a degraded path" nuance. Unlike geocoding (which simply vanishes when unsupported), media picking has a full web alternative: the browser's own
<InputFile>. SoIsSupportedfalse here does not mean "you cannot upload a photo," it means "do not draw the native picker button; the component draws the standard file input instead." The null default's job is only to signal that switch. Same [Rubric §2 - Design Patterns] and [Rubric §1 - SOLID] framing asNullGeocodingService; additionally [Rubric §18 - UI Architecture], which assesses how presentation concerns are separated, is visible here because the choice between native picker andInputFileis 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) andCapturePhotoAsync(NullMediaPickerService.cs:17) both returnTask.FromResult<PickedMedia?>(null). Because they returnnullrather than a livePickedMedia, there is noStreamto dispose, matching the "dispose after upload" ownership rule the real type documents. - 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 onInputFile, so the native picker abstraction exists only to give MAUI heads a camera/library flow; making the default inert keeps the shared avatar component host-agnostic. - Where it's used - registered by
AddDeviceCapabilityDefaults(DependencyInjection.cs:52); overridden byMauiMediaPickerServiceon native heads. Web and Server heads keep this default and fall back toInputFile.
NullPushDeviceTokenProvider
MMCA.Common.UI ·
MMCA.Common.UI.Services.Capabilities.Fallbacks·MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Capabilities/Fallbacks/NullPushDeviceTokenProvider.cs:9· Level 2 · class
- What it is - a push token provider that never produces a token:
IPushDeviceTokenProviderimplemented to always returnnull. It is the default everywhere, including native heads, until an app plugs in a credentialed Firebase/APNs provider. - 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 is the default even on native heads that can receive push, because push also needs external credentials (an FCM/APNs key) that a plain build does not carry. Returning
nullleaves the entire registration pipeline present and correctly ordered but dormant, which is precisely the state a build without push credentials should sit in. Swapping in a real provider (a plainAddafter the defaults) activates the pipeline with no other change. Note there is noIsSupportedprobe on this contract at all; token presence is the signal. Same [Rubric §2 - Design Patterns] framing asNullGeocodingService; [Rubric §7 - Microservices Readiness / composition] is loosely relevant in that the token source is a pluggable edge dependency the app supplies rather than framework-baked. - Walkthrough -
sealed class(NullPushDeviceTokenProvider.cs:9). A single method,GetTokenAsync(NullPushDeviceTokenProvider.cs:12), returnsTask.FromResult<PushDeviceToken?>(null). There is deliberately noIsSupportedmember; the contract has 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 overridable pieces on purpose:MMCA.Common.UI.Mauioverrides the registration service while the token provider stays null until the app supplies real credentials, so even native heads are registered-but-tokenless out of the box (DependencyInjection.cs:45). - Where it's used - registered by
AddDeviceCapabilityDefaults(DependencyInjection.cs:49), alongsideNullPushRegistrationService. Consumed by the push registration flow; an app overrides it once real FCM/APNs credentials exist.
WebFormFactor
MMCA.Common.UI.Web ·
MMCA.Common.UI.Web.Services·MMCA.Common.UI.Web/Services/WebFormFactor.cs:12· Level 1 · class
- What it is: The Blazor Server side implementation of
IFormFactor, the tiny contract that lets shared UI adapt to the host it is running on. It reports the form factor as the literal string"Web"because this code executes on the server during SSR prerender and interactive Server render mode. - Depends on: First-party: the
IFormFactorcontract it implements (MMCA.Common.UI.Web/Services/WebFormFactor.cs:12). Externals:System.Environment(BCL) for the OS description. No app-specific state, which is why it was hoisted out of the individual Blazor Web hosts into the shared package. - Concept introduced: Host-selected capability implementation.
IFormFactoris the smallest example of the pattern this whole group is built around: one interface, and a different concrete class registered per host at DI composition time. The three siblings are this class (WebFormFactorfor Blazor Server),WasmFormFactorin MMCA.Common.UI for WebAssembly, andMauiFormFactorin MMCA.Common.UI.Maui for the native head. Shared components depend only on the interface; the host picks the body. [Rubric §18, UI Architecture] assesses how cleanly presentation concerns are layered and how portable components are across render hosts; a one-method contract with three swappable bodies keeps every consuming component host-agnostic. [Rubric §22, Responsive/Cross-Browser] assesses how the app adapts to device and environment;GetFormFactor()is the coarse signal components branch on when server-rendered behavior must differ from WASM or native. - Walkthrough: The class is
sealed(WebFormFactor.cs:12) and holds no fields.GetFormFactor()returns the constant"Web"(WebFormFactor.cs:15); it is a constant rather than a probe because Blazor Server always runs this code server-side.GetPlatform()returnsEnvironment.OSVersion.ToString()(WebFormFactor.cs:18), the server OS description, which for a server render is the deployment host rather than the end user's device. - Why it's built this way: Server prerender and interactive Server render both run on the server, so there is no reliable client device signal available at this layer; reporting
"Web"and the server OS is the honest answer for this host. Keeping the type stateless and app-neutral is what allowed it to move up into the sharedMMCA.Common.UI.Webpackage (its XML doc notes it is registered viaAddCommonWebFormFactor(),WebFormFactor.cs:9-10). - Where it's used: Registered by the Blazor Web hosts through
AddCommonWebFormFactor(); 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 client device; do not treat it as a client fingerprint. Not determinable from source: the exact registration body ofAddCommonWebFormFactor()(it lives in the Web host's own DI file, outside this unit).
DependencyInjection
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/DependencyInjection.cs:15· Level 3 · class (static)
- What it is: The service-level registration surface for the MAUI native device-capability layer (ADR-042). It adds one
AddMauiDeviceCapabilities()method that binds every capability contract the framework backs natively to its MAUI implementation, plus a separateAddMauiFormFactor()for the nativeIFormFactor. - Depends on: First-party: the whole capability contract set in
MMCA.Common.UI.Services.Capabilitiesand theirMaui*implementations inMMCA.Common.UI.Maui.Capabilities(DependencyInjection.cs:2-4),MauiFormFactor(DependencyInjection.cs:69-70). Externals:Microsoft.Extensions.DependencyInjection.IServiceCollection(BCL/NuGet) as the extended type. - Concept introduced:
extension(IServiceCollection)registration blocks. The class is astatic classwhose members live inside anextension(IServiceCollection services)block (DependencyInjection.cs:17), the C# preview extension-member syntax this codebase uses everywhere for DI. The methods appear as instance methods onIServiceCollectionat call sites (builder.Services.AddMauiDeviceCapabilities()). Two registration lifetimes appear here and the code explains both in comments: singletons for the capability services because a MAUI head is single-user and the stateful ones (connectivity, battery) wrap app-global platform events (DependencyInjection.cs:27-28), and one scoped registration forIExternalAuthBrokerbecause it navigates through the circuit'sNavigationManager(DependencyInjection.cs:56-59). [Rubric §10, Cross-Cutting] assesses how infrastructure concerns are composed rather than scattered; centralizing every native binding in one extension method keeps the host program short. [Rubric §7, Microservices Readiness] assesses how cleanly a host swaps implementations; last-registration-wins over the shared TryAdd defaults is exactly that boundary. - Walkthrough:
AddMauiDeviceCapabilities()(DependencyInjection.cs:25) registers sixteen capability contracts as singletons in a block (DependencyInjection.cs:29-45): 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 are commented because they carry conditions:IPushRegistrationServiceis wired but stays inert until a credentialedIPushDeviceTokenProviderexists (ADR-044,DependencyInjection.cs:47-50);IMediaPickerServiceneeds the head to declare camera permissions for capture (ADR-045,DependencyInjection.cs:52-54); andIExternalAuthBrokeris registeredAddScopedand stays inert (IsAvailable == false) until the head configuresOAuth:MobileRedirectSchemeand the platform callback (DependencyInjection.cs:56-59).AddMauiFormFactor()(DependencyInjection.cs:69-70) is deliberately separate so a head that registers its ownIFormFactorkeeps last-registration-wins control. - Why it's built this way: The class doc is explicit that plain
Add(notTryAdd) is used and must run afterAddUISharedso these native bodies override the shared TryAdd null-object defaults (last registration wins,DependencyInjection.cs:11-13). Splitting form-factor registration from capability registration (DependencyInjection.cs:63-68) preserves that override control per concern. Contracts with no native body yet (biometrics broker gaps, speech, external-auth) simply keep their shared null defaults until their feature wave lands (DependencyInjection.cs:20-24), so the DI graph always resolves. - Where it's used: Called by
HostingDependencyInjection'sUseMauiDeviceCapabilities()(HostingDependencyInjection.cs:28), the builder-level entry point heads actually call; the class doc namesbuilder.UseMauiDeviceCapabilities()as the preferred path (DependencyInjection.cs:9-11). - Caveats / not-in-source: "Wired but inert" is a real runtime state for push, media capture, and external auth: registration does not imply the capability works without the additional host config each comment names. Not determinable from source: the bodies of the individual
Maui*Serviceimplementations (they live in other units of this group).
DeviceCapabilitiesInitializer
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/DeviceCapabilitiesInitializer.cs:15· Level 3 · class
- What it is: A MAUI startup hook that bridges local-notification taps into Blazor routing. It implements
IMauiInitializeService, so itsInitializeruns when the app is built, and it forwards the route carried by a tapped reminder to the sharedIDeepLinkDispatcher. - Depends on: First-party:
IDeepLinkDispatcher(DeviceCapabilitiesInitializer.cs:2). Externals:Microsoft.Maui.Hosting.IMauiInitializeService(MAUI), andPlugin.LocalNotificationwith itsNotificationActionEventArgs(NuGet,DeviceCapabilitiesInitializer.cs:3-4). - Concept introduced: Cold-start deep-link buffering. This type is the native publisher end of the deep-link funnel; the receiver end is the
DeepLinkListenercomponent in the shared layout. When a notification is tapped while the app is running, the route is delivered live viaIDeepLinkDispatcher'sRouteRequestedevent; when the tap cold-starts the app, the dispatcher buffers the single pending route until first render, and the listener drains it (DeviceCapabilitiesInitializer.cs:10-13). [Rubric §25, Navigation & IA] assesses how navigation intent flows through the app; routing every native entry point (taps, app links) through one dispatcher keeps Blazor routing the single source of truth. [Rubric §29, Resilience & Business Continuity] assesses graceful handling of edge states; the cold-start buffer means a tap that launches the process is not silently lost before the router exists. - Walkthrough: The class is
sealed(DeviceCapabilitiesInitializer.cs:15).Initialize(IServiceProvider services)null-guards its argument (DeviceCapabilitiesInitializer.cs:20), then resolvesIDeepLinkDispatcherwithGetServiceand returns early if none is registered (DeviceCapabilitiesInitializer.cs:22-26), so a head without the dispatcher is a no-op rather than a crash. When present, it subscribes toLocalNotificationCenter.Current.NotificationActionTapped(DeviceCapabilitiesInitializer.cs:28). The private handlerOnNotificationTapped(DeviceCapabilitiesInitializer.cs:31) ignores dismissals (IsDismissed,DeviceCapabilitiesInitializer.cs:33-36), reads the app-relative route fromargs.Request?.ReturningData(DeviceCapabilitiesInitializer.cs:38), and only publishes when the route is non-blank (DeviceCapabilitiesInitializer.cs:39-42). - Why it's built this way: Notification metadata is not routing; this initializer translates the plugin's tap event into the codebase's own
IDeepLinkDispatchervocabulary so the shared listener never touchesPlugin.LocalNotification. Wiring it as anIMauiInitializeServicemeans the subscription is established once at app build time. The defensive early-return keeps the hook safe to register unconditionally. - Where it's used: Registered by
HostingDependencyInjectionas anIMauiInitializeServicesingleton (HostingDependencyInjection.cs:29); its published routes are consumed by the sharedDeepLinkListenercomponent throughIDeepLinkDispatcher. - Caveats / not-in-source: The route contract is entirely
ReturningDataon the scheduled notification: a reminder created without an app-relative route in that field produces no navigation. Not determinable from source: theDeepLinkListenercomponent body and the code that schedules notifications withReturningDataset (both outside this unit).
HostingDependencyInjection
MMCA.Common.UI.Maui ·
MMCA.Common.UI.Maui·MMCA.Common.UI.Maui/HostingDependencyInjection.cs:10· Level 4 · class (static)
- What it is: The
MauiAppBuilder-level entry point for the entire device-capability layer (ADR-042). Its one methodUseMauiDeviceCapabilities()composes the service registrations plus the platform hooks that need the builder itself, so a head configures native capabilities with a single fluent call. - Depends on: First-party:
DependencyInjection'sAddMauiDeviceCapabilities()(HostingDependencyInjection.cs:28) andDeviceCapabilitiesInitializer(HostingDependencyInjection.cs:29). Externals:Microsoft.Maui.Hosting.MauiAppBuilder(MAUI) as the extended type, andPlugin.LocalNotification'sUseLocalNotification()(NuGet,HostingDependencyInjection.cs:2,27). - Concept introduced: Builder-level vs service-level composition. This is the layered pairing that runs through MAUI hosting:
DependencyInjectionregisters services onIServiceCollection, while this class operates onMauiAppBuilderbecause two of the steps (the Plugin.LocalNotification lifecycle wiring and the initializer registration) need more than the service collection. It uses the sameextension(MauiAppBuilder builder)block syntax (HostingDependencyInjection.cs:12). [Rubric §16, Maintainability] assesses how easy the framework is to adopt correctly; folding three easy-to-forget steps into one fluent call reduces the ways a head can be misconfigured. [Rubric §17, DevOps] assesses reproducible, low-ceremony host setup; a single builder extension is that ceremony reduction for the native head. - Walkthrough:
UseMauiDeviceCapabilities()(HostingDependencyInjection.cs:25) does three things in order:builder.UseLocalNotification()to initialize the notification plugin (HostingDependencyInjection.cs:27),builder.Services.AddMauiDeviceCapabilities()to bind every native capability (HostingDependencyInjection.cs:28), andAddSingleton<IMauiInitializeService, DeviceCapabilitiesInitializer>()to register the notification-tap deep-link bridge (HostingDependencyInjection.cs:29). It returnsbuilderfor chaining (HostingDependencyInjection.cs:30). - Why it's built this way: The class doc pins the ordering constraint: call this AFTER
AddUISharedinMauiProgram.CreateMauiApp(HostingDependencyInjection.cs:8-9), becauseDependencyInjectionuses plainAddto override the shared TryAdd defaults. One step it deliberately cannot do for the head is the MauiCommunityToolkit registration: speech-to-text depends on.UseMauiCommunityToolkit(), and the toolkit's MCT001 analyzer requires that call to appear directly in the app's ownUseMauiApp<T>()chain, so the wrapper documents the requirement rather than hiding it (HostingDependencyInjection.cs:18-23). - Where it's used: Called once per MAUI head in
MauiProgram.CreateMauiApp; it is the public front door the class docs steer heads toward instead of callingDependencyInjectiondirectly (DependencyInjection.cs:9-11). - Caveats / not-in-source: The head still has two obligations this wrapper cannot fulfill: chaining
.UseMauiCommunityToolkit()for speech-to-text, and providing the per-capability config each inert service needs (push credentials, camera permissions, OAuth redirect scheme). Not determinable from source: the concreteMauiProgramof any downstream head that calls this method (outside this unit).
⬅ ADC Application Host, UI Shell & Cross-Module Composition • Index • Testing & Quality Infrastructure ➡