Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

AppDeviceBridge Releases

Feature
Barcodes bridge. Shiny.AppDeviceBridge.Barcodes, backed by Shiny.Maui.Controls 2.3.1, adds /_bridge/barcodes: the device’s camera as a barcode scanner for the page. POST barcodes/scan shows a full-screen scanner over the web app and answers when it closes, with every code read (value, format, bounds) and why it closed. A refused permission, a dismissal or a timeout comes back as the reason, not as an error. maxResults (1 by default; 0 for as many as the person scans), formats, filterDuplicates, timeoutSeconds, scanWindow, torchOn, title and instructions shape the scan. barcodes.detected reports each code as it’s read, and DELETE barcodes/scan stops the scan. mode: "LongRange" uses the controls’ new long-range engine, which reads codes the platform scanners miss, such as a shelf label across an aisle. With autoZoom, it zooms in on a code too small to read. maxAutoZoom, fullFrameInterval, maxNativeCropsPerFrame and uncheckedConfirmations tune it. The device runs one scan at a time (409 scan_in_progress). Register it with bridge.AddBarcodesBridge(). The client is IBarcodesBridge in Shiny.AppDeviceBridge.Barcodes.Client, or BarcodesBridge in TypeScript. iOS, Mac Catalyst and macOS (Apple Vision), Android (ML Kit) and Windows (ZXing.Net); Linux answers 501. The Camera and Desktop bridges move to Shiny.Maui.Controls 2.3.1 with it. See Barcodes.
Feature
Hubs bridge. Shiny.AppDeviceBridge.Hubs, backed by Shiny.UniversalHubs 1.5, adds /_bridge/hubs: Universal Hubs whose logic lives in the downloaded web app, so a hub’s behavior ships with a web app update. The app serves them to nearby devices over Bluetooth LE, Wi-Fi (mDNS) and Multipeer on the bridge’s own HTTP server, and every client call reaches the web app as the native call hub:{name} (the page while it is open, otherwise background.js), carrying a HubCall and answered with a HubCallReply. PUT hubs defines a hub (name, characteristic, an allowed service type, its methods, maxClients, allowTunnel). Defining it again with the same values keeps it and its clients, and only the methods may change; a new characteristic, service type, client limit or tunnel setting replaces it and disconnects its clients. POST hubs/{name}/start and /stop run it, DELETE hubs/{name} removes it, and connections, push (to everyone, or to connections and groups less exceptions) and groups manage its clients. Methods not in the definition are answered HubMethodNotFound without reaching the web app. A stream replies once started and posts its items to hubs/streams/{streamId}, then /end, and hubs.stream says when its caller has left. Errors keep the web app’s type for clients (HubError in JavaScript, HubException in C#), and an abort ends the caller’s session after the reply. With no page or background.js to answer, connections are refused and calls fail with HubUnavailable. Register it with bridge.AddHubsBridge(server => server.AllowDynamicHubs(o => o.ServiceTypes.Add("_myapp._tcp"))), where server is Universal Hubs’ AddUniversalServer builder, so compiled hubs can sit beside. In Blazor, derive from WebAppHub, register it with services.AddWebAppHub<GameHub>(new("game", uuid, "_myapp._tcp")), then WebAppHubs.DefineAsync() and StartAsync("game"). The client is IHubsBridge in Shiny.AppDeviceBridge.Hubs.Client, or HubsBridge in TypeScript, and a plain page imports hostHub from /_bridge/hubs/client.js. The new IWebAppNativeCalls in Shiny.AppDeviceBridge.Client lets a client package take native calls without depending on Blazor. Wi-Fi works on every head. Bluetooth LE works on Android, iOS and Mac Catalyst, and on Windows and macOS once the app calls services.AddBluetoothLeHosting(). Without it, and on Linux, BLE is skipped and the hubs run over Wi-Fi and Multipeer. Multipeer works on Apple platforms with Shiny.UniversalHubs.Multipeer. A transport that can’t start is skipped, and when none starts the request answers 503. A host without AddHubsBridge answers 501. This is a security change: the hub routes live at /hubs, outside the bridge prefix, so the bridge policy doesn’t apply to them; each hub’s own rules do (MaxClients, the web app’s connection refusal, AllowTunnel). Wi-Fi clients reach them only when the app binds beyond loopback, while /_bridge stays on-device only, and remote devices’ arguments reach web app code, so treat them as untrusted. The web app can also join hubs on other devices, compiled or dynamic, through Universal Hubs’ dynamic hub clients: POST hubs/clients creates a client ({ characteristicUuid, serviceType }, at most 8, 409 too_many_clients), clients/{id}/discover reports hosts as hubs.client.host (listen first, 409 not_listening; it stops when nothing listens), clients/{id}/connect joins a discovered host by name (404 if unseen) or a url (Wi-Fi only) with name, prefer, allowed and properties (409 refused when the host refuses), and invoke and streams call by name with JSON values (409 not_connected before connecting). A hub’s own failure is the result’s error, not an HTTP failure. Pushes, status and stream items arrive as hubs.client.push, hubs.client.status and hubs.client.stream. AddHubsBridge’s new second parameter configures the clients (clients => clients.Multipeer("myapp-mc")), and clients is a reserved hub name. See Hubs.
Feature
AI conversation bridge. Shiny.AppDeviceBridge.AiConversation, backed by Shiny.AiConversation 3.0, adds /_bridge/ai: conversations with the app’s own AI model, whose key, tools and prompts never reach the page. POST ai/sessions starts a stored, resumable session, and GET ai/sessions/latest carries on the most recent one. POST ai/sessions/{id}/messages sends text, an answer to a question with choices, images from file roots and per-turn instructions. The reply streams as ai.delta and ends with ai.turn.ended, or with "wait": true the request answers once the turn ends. A turn outlives the request that started it, a newer message supersedes it, and DELETE ai/sessions/{id}/turn interrupts it. The model’s questions arrive as ai.questions with their choices. History pages backwards with each message’s parts and image attachments, and GET/PUT/DELETE ai/memory manage what the assistant remembers. With o => o.Voice = true, POST ai/voice attaches the hands-free voice loop (wake word, push-to-talk, talking over a reply) to a session. It runs only while the page listens to ai.voice.state. Register it with bridge.AddAiConversationBridge(ai => ai.SetChatClientProvider<T>()). The client is IAiConversationBridge in Shiny.AppDeviceBridge.AiConversation.Client, or AiConversationBridge in TypeScript. Sessions run on every platform. The voice loop runs on iOS, Mac Catalyst, macOS, Android and Windows, and on Linux it needs a cloud or Whisper speech service; the bridge answers 501 for whatever isn’t registered. This is a new security surface: a page that can reach the bridge spends the app’s model and can lead it to call any tool the app registered. See AI Conversation.
Feature
Audio bridge. Shiny.AppDeviceBridge.Audio, backed by Shiny.Audio 3.0, adds /_bridge/audio for the device’s speakers and microphone. POST audio/playback plays a clip from an absolute http or https URL or a file in a file root and answers with its id. Clips play together, DELETE audio/playback/{id} stops one, and audio.playback.ended says how each ended. PUT audio/volume sets the system media volume, and audio.volume reports every change. POST audio/recording records the microphone to 16 kHz WAV, optionally through an effect preset (Robot, Chipmunk, DeepVoice, Cathedral, Telephone, Megaphone, Ensemble) and keeping the clean take as well. DELETE audio/recording files it into the data root under audio-recordings/, and the app’s MaxRecordingDuration (one hour by default) stops and files a recording left running. GET audio/devices lists the audio routes, and POST audio/devices/picker shows the system output picker. POST audio/monitor routes the microphone live to the output with adjustable gain. It runs only while the page listens to audio.monitor.level. A recording and the monitor share one microphone (409 microphone_busy). Register it with bridge.AddAudioBridge(). The client is IAudioBridge in Shiny.AppDeviceBridge.Audio.Client, or AudioBridge in TypeScript. Playback, recording and routes run on iOS, Mac Catalyst, macOS, Android, Windows and Linux. The monitor runs on all of those except Windows, which answers 501, and setting the volume answers 501 on iOS and Mac Catalyst. See Audio.
Enhancement
Speech tone. POST speech/speak takes an optional tone: an emotion (Happy, Excited, Calm, Whispering, and others), an intensity from 0 to 2, and free-text instructions. It passes through to Shiny.Speech’s SpeechTone, so an expressive cloud engine registered in place of the on-device one (Azure, ElevenLabs v3, Typecast, OpenAI) delivers it, and the on-device engines ignore it. An intensity outside 0–2, or instructions over 1,000 characters, answers 400. All platforms with a speech engine. See Speech.
Feature
NFC bridge. Shiny.AppDeviceBridge.Nfc, backed by the new Shiny.Nfc 6.5, adds /_bridge/nfc for reading and writing NDEF tags: URLs, text, MIME payloads, smart posters and Android app records. A WebView has no Web NFC, so this is the page’s only way to reach the radio. POST nfc/read and POST nfc/write handle one tap. To keep scanning, listen for nfc.tag, which delivers each tag with its message already read, then POST nfc/scan. The scan ends with DELETE nfc/scan, when the page stops listening, or when the platform ends it (nfc.scan). Scanning with holdSeconds keeps each tag available to POST nfc/tags/{tagId}/write and /lock until the page releases it. Records to write are a URI, text, MIME, Android package or raw record. Tag failures answer with codes such as 410 tag_lost, 409 tag_read_only and 413 message_too_large, and only one NFC session runs at a time (409 nfc_busy). Register it with bridge.AddNfcBridge(). The client is INfcBridge in Shiny.AppDeviceBridge.Nfc.Client, or NfcBridge in TypeScript. Android, iOS, and PC/SC readers on Windows, macOS and Linux; Mac Catalyst and devices without NFC answer 501. See NFC.
Feature
Biometrics bridge. Shiny.AppDeviceBridge.Biometrics, backed by the new Shiny.Biometrics 6.5, adds /_bridge/biometrics: the platform’s own biometric prompt for the page. GET biometrics reports, with no UI, whether biometrics are available, enrolled or locked out, the hardware present (Fingerprint, Face, Iris, or Unknown for Windows Hello) and whether a passcode is set. POST biometrics/authenticate with a reason shows the prompt, and allowDeviceCredential lets the user use their passcode or PIN instead. A cancel, lockout or missing enrollment comes back as the result’s status rather than an error. A missing NSFaceIDUsageDescription or Android activity answers 409 cannot_prompt. Register it with bridge.AddBiometricsBridge(). The client is IBiometricsBridge in Shiny.AppDeviceBridge.Biometrics.Client, or BiometricsBridge in TypeScript. iOS, Mac Catalyst and macOS (Touch ID, Face ID, Optic ID), Android (fingerprint, face, iris) and Windows (Windows Hello, which always offers its PIN); Linux answers 501. The answer is a local yes/no for the page’s UI, not proof of identity for a server. See Biometrics.
Feature
Share target bridge. Shiny.AppDeviceBridge.ShareTarget adds /_bridge/sharetarget, which makes the app a place other apps can share text, links and files to. Each share waits in a queue for the page: across restarts, until the page removes it, with the oldest dropped past MaxPending (20). Its files are copied into a file root (data by default) under shares/{id}/ and read through the files bridge. GET sharetarget/pending lists the queue, GET and DELETE sharetarget/pending/{id} read or remove one share (removing deletes its files), and sharetarget.received fires while the page is open. A share’s url is only ever an absolute http or https URL. Text that is nothing but such a link arrives as the url, and any other scheme is kept as text. File names are cleaned to one path segment. Register it with bridge.AddShareTargetBridge(). Platform code the bridge does not hook, such as a share extension or the AppKit app delegate, hands content over with ShareTarget.ReceiveAsync. The client is IShareTargetBridge in Shiny.AppDeviceBridge.ShareTarget.Client, or ShareTargetBridge in TypeScript. Android (send intents; content: files only), iOS and Mac Catalyst (files through “Open in” and “Copy to”; text and links need the app’s own share extension), Windows (the Share Target contract and file activation), macOS through the app delegate, and Linux from the command line with AcceptCommandLineFiles. Without its file root, the bridge answers 501. See Share Target.
Feature
Age signals bridge. Shiny.AppDeviceBridge.AgeSignals, backed by Shiny.AgeSignals 6.4, adds /_bridge/agesignals: the user’s age range as the app store shares it, through Apple Declared Age Range and Google Play Age Signals, for app store accountability laws such as Texas SB 2420. POST agesignals/request with one to three ascending age gates answers the range, how it was established (SelfDeclared, GuardianDeclared, Confirmed, IdVerified), and whether it was shared, declined or needs verification. GET agesignals reports, with no UI, whether the OS has the API and Apple’s eligibility and regulatory flags, and POST agesignals/significant-update shows Apple’s significant-update sheet. Store failures answer with codes such as 503 network and 403 app_not_owned. Register it with bridge.AddAgeSignalsBridge(); Apple also needs the com.apple.developer.declared-age-range entitlement. The client is IAgeSignalsBridge in Shiny.AppDeviceBridge.AgeSignals.Client, or AgeSignalsBridge in TypeScript. iOS, macOS and Android; Mac Catalyst, Windows and Linux answer 501. See Age Signals.
Feature
VPN bridge. Shiny.AppDeviceBridge.Vpn, backed by Shiny.Net.Vpn 6.4, adds /_bridge/vpn for the app’s profile in the operating system’s built-in IKEv2 VPN client. The page saves a profile authenticated by a shared secret, a username and password, or a PKCS#12 client certificate (the first save shows the OS consent prompt and answers Denied if the user declines), removes it, connects and waits for the tunnel, disconnects, and follows the tunnel through the vpn.status event, including changes made in system settings. GET vpn reports the state and what the platform can do, and an option it lacks answers 501. Connection failures answer 502 vpn_connection_failed with the OS’s reason. Register it with bridge.AddVpnBridge(); Apple also needs the Personal VPN entitlement. The client is IVpnBridge in Shiny.AppDeviceBridge.Vpn.Client, or VpnBridge in TypeScript. iOS, Mac Catalyst, macOS, Android 11+ (Android 13 to report the state) and Linux through NetworkManager’s strongSwan plugin; Windows answers 501. This is a new security surface: a page that can save a profile decides where the device’s traffic goes, so register it only for a web app you ship. See VPN.
Enhancement
Shiny 6.4. Every Shiny-backed bridge now builds on Shiny 6.4.0, up from 6.3.0. The Camera and Desktop bridges move to Shiny.Maui.Controls 2.1.0, whose desktop features register through one UseShinyDesktop() call. AddTrayIconBridge() and AddQuickEntryBridge() make that call for you, so app code is unchanged. The server builds on Shiny.Net.HttpServer 2.2.1 and the settings bridge on Shiny.Extensions.Stores 6.0.1.
Fix
A Bluetooth LE read, write or notification cut off by a disconnect no longer reports an unobserved task exception. When the page’s request or subscription had already ended, the operation’s late BleException surfaced through TaskScheduler.UnobservedTaskException, which an app forwarding those to crash telemetry logged as an error on every disconnect. It is now dropped. A request still waiting gets its error as before. From Shiny.BluetoothLE 6.3.1. All platforms. See Bluetooth LE.
FixiOS
An exception while Bluetooth LE handles an adapter state change or a state restoration no longer crashes the app. CoreBluetooth calls these from async void callbacks, so an exception there ended the process; it is now logged. On Android, a connection-state subscriber that throws is logged too, instead of reporting an unobserved task exception. From Shiny.BluetoothLE 6.3.1. iOS, Mac Catalyst, macOS and Android.
Fix
A printer scan or sweep that fails after the page stopped listening no longer reports an unobserved task exception. A network scan’s mDNS browse or subnet sweep could fail after printers/scan had ended, and the failure went unobserved. It is now dropped. From Shiny.Printers 6.3.1. All platforms. See Printing.
FixAndroid
DELETE notifications/{id} removes a notification that was sent straight away and is still showing. Shiny only stored scheduled, repeating and geofence notifications, so cancelling one sent with no schedule found nothing and left it in the notification shade. iOS already removed it. From Shiny.Notifications 6.4.0. See Notifications.
Fix
TypeScript: binary fields in a request body are base64 strings. The generated clients typed a byte array inside a JSON body as Blob | ArrayBuffer | Uint8Array, but the body is sent with JSON.stringify, which writes those as {} or {"0":…}, so the bridge refused it. They are now string, carrying base64, as the native bridge reads them. This changes BleWriteRequest.data, so Bluetooth LE write from TypeScript now takes base64 (for example btoa(String.fromCharCode(...bytes))). Raw bodies, such as a file write or a PDF to print, still take binary.
Feature
Game center bridge. Shiny.AppDeviceBridge.GameCenter, backed by Shiny.GameCenter 6.3, adds /_bridge/games for Apple Game Center and Google Play Games. The page signs the player in, unlocks achievements, sets or increments their progress, reveals hidden ones, submits scores, and reads achievements, leaderboard scores (global or friends, today, this week or all time) and friends, and opens the services’ own achievement and leaderboard screens. Unlocks, progress and scores are queued on the device and sent once the player is signed in and online, so they succeed offline; GET games reports pendingCount and POST games/flush sends the queue now. Achievements and leaderboards are named by the keys the app registers with bridge.AddGameCenterBridge(o => o.AddAchievement(…).AddLeaderboard(…)). games.player fires on sign-in, sign-out and account switches. Service failures answer with codes such as 401 not_authenticated, 404 invalid_id and 503 network. The client is IGameCenterBridge in Shiny.AppDeviceBridge.GameCenter.Client, or GameCenterBridge in TypeScript. iOS, Mac Catalyst, macOS and Android; Windows and Linux answer 501. See Game Center.
Feature
Call directory bridge. Shiny.AppDeviceBridge.CallDirectory, backed by Shiny.Mobile.CallDirectory 6.3, adds /_bridge/calldirectory for call blocking and caller identification. The page adds, removes, replaces and clears a list of numbers to Block, Identify (ring with a label as caller ID) or Allow (Android: always ring, even when unlisted callers are blocked), reloads it into the system and reads how the load went, and turns numbers as people write them into the stored form with POST calldirectory/normalize. GET calldirectory reports whether the user turned the feature on and how many numbers it holds; POST calldirectory/access opens Settings on iOS and asks for the call screening role on Android. Register it with bridge.AddCallDirectoryBridge(); on iOS the app’s project also sets ShinyCallDirectoryExtension and ShinyCallDirectoryAppGroup. The client is ICallDirectoryBridge in Shiny.AppDeviceBridge.CallDirectory.Client, or CallDirectoryBridge in TypeScript. iOS and Android 10+; every other platform answers 501. See Call Directory.
Feature
Receipt printer status and subnet sweeps. GET printers/connection/status (GetPrinterStatusAsync(), getPrinterStatus() in TypeScript) reads the connected printer’s sensors: online, cover open, paper near its end or out, cutter and other errors, the cash-drawer pin, and canPrint. It works for ESC/POS printers on the network, which ConnectedPrinter.supportsStatus reports; other printers answer 501, and one that doesn’t reply answers 504 printer_timeout. A network scan with sweep: true also finds printers that don’t advertise over mDNS by trying a TCP connection to every host on the device’s subnets on the allowed printer ports, and each network result’s discovery says how it was found. Built on Shiny.Printers 6.3. All platforms. See Printing.
Enhancement
Notification actions that require unlocking. A channel action’s requiresUnlock makes the user unlock the device before the action runs, for actions with a real-world effect. iOS, Mac Catalyst and macOS, and Android 12 and later; older Android versions run the action without asking, and Windows and Linux ignore it. In TypeScript, NotificationChannelAction now has a required requiresUnlock field. See Notifications.
Enhancement
applicationIdentifier in GET app/info, from Shiny.Core’s IPlatform: the bundle identifier on Apple, the package name on Android, the package identity name on a packaged Windows app, or the Flatpak app id or Snap name on Linux, and null when the app has none. All platforms.
Enhancement
Shiny 6.3. Every Shiny-backed bridge now builds on Shiny 6.3.0, up from 6.1.1, and the Camera and Desktop bridges on Shiny.Maui.Controls 1.7.0. GET app/info’s deviceIdiom now comes from Shiny’s DeviceFormFactor, with the same values.
BREAKING
Shiny 6. Every Shiny-backed bridge now builds on Shiny 6.1.1. Shiny.AppDeviceBridge.Maui starts Shiny’s MAUI host (UseShiny()) on every head, including the macOS (AppKit) and Linux (GTK4) heads, which Shiny.Hosting.Maui 6 now ships. The bridges no longer register Shiny’s core services for themselves on macOS. UseAppDeviceBridge also registers Shiny.Core’s IBattery and IConnectivity. HttpTransfers on Linux uses Shiny.Core’s connectivity, so it no longer depends on Shiny.Core.Linux.
BREAKING
Shiny.AppDeviceBridge.AppSupport, .AppSupport.Client and .AppSupport.Linux are removed. The app support bridge is split in two. /_bridge/app moves into Shiny.AppDeviceBridge itself, over Shiny.Core and Shiny.AppStore, and is registered with bridge.AddAppBridge(store, startup). It serves info, orientation, the screen, settings, the store, launch at login, battery and connectivity, with no MAUI dependency. /_bridge/device is new in Shiny.AppDeviceBridge.Maui, over .NET MAUI Essentials, and is registered with bridge.AddDeviceBridge(). It takes the browser, maps, sharing, haptics, vibration, keep-awake and the clipboard, so app/browser, app/map, app/share, app/haptics, app/vibrate and app/clipboard become device/…, and app/screen/keep-awake becomes device/keep-awake. The sensors bridge moves to Shiny.AppDeviceBridge.Maui with bridge.AddSensorsBridge(); its routes are unchanged. IAppBridge, IDeviceBridge and ISensorsBridge and their contracts are now in Shiny.AppDeviceBridge.Client, registered with AddAppBridgeClient(), AddDeviceBridgeClient() and AddSensorsBridgeClient(). In TypeScript, the app-support module is gone: AppBridge, DeviceBridge and SensorsBridge come from the package root as before. Contract changes: the browser and map calls return DeviceActionResult. ScreenInfo drops rotation and keepScreenOn, and its refreshRate is a double. app.battery now carries BatteryInfo, energy saver included, and app.energysaver is removed. AppStoreOptions and StartupServiceOptions become Shiny.AppStore’s AppStoreOptions and Shiny.Core’s AppStartupOptions. AddAppSupportLinux() is removed: the Linux (GTK4) head reads the battery from Shiny.Core’s sysfs implementation, with change events. Platforms are unchanged; each feature still answers 501 where its platform lacks it. See App & Device.
BREAKING
Shiny.AppDeviceBridge.DocumentGeofencing is replaced by Shiny.AppDeviceBridge.RegionGeofencing, following Shiny.DocumentDb.Geofencing and Shiny.DocumentDb.Geo’s move into Shiny 6.1 as Shiny.Geofencing.Regions and Shiny.Geofencing.Regions.DocumentDb. AddDocumentGeofenceBridge(cfg => cfg.AddRegionSet<T>(…)) becomes AddRegionGeofenceBridge(cfg => cfg.AddDocumentRegionSet<T>(…)), and the reference dataset’s sets are cfg.AddNorthAmericanRegions() and cfg.AddNorthAmericanCities(), seeded by services.AddNorthAmericaSeeder() with the store mapped by opts.MapNorthAmericaData(). The routes move from /_bridge/documentgeofences to /_bridge/regiongeofences, the event from documentgeofence.change to regiongeofence.change, and the background.js handler from documentgeofence to regiongeofence. The client is IRegionGeofencesBridge in Shiny.AppDeviceBridge.RegionGeofencing.Client (AddRegionGeofencesBridgeClient()), or RegionGeofencesBridge in TypeScript. Its contracts drop the Document prefix (RegionChange, CurrentRegion, RegionGeofenceStatus, RegionGeofencingJsonContext), and RegionSetInfo no longer carries withinMeters. WebAppDocumentGeofenceDelegate and WebAppDocumentGeofenceOptions become WebAppRegionGeofenceDelegate and WebAppRegionGeofenceOptions. region is no longer written through the document store’s options by default: set o.RegionSerializerOptions to the store’s JsonSerializerOptions. start no longer answers 501 spatial_not_supported, because a store without spatial queries now fails when the app starts. Platforms are unchanged: Android, iOS and Mac Catalyst, and 501 elsewhere. See Region Geofencing.
Feature
Geocoder and custom region sets. Besides documents, the region geofencing bridge monitors sets resolved by reverse geocoding (cfg.AddGeocoderRegionSet("states", GeocoderRegionLevel.AdministrativeArea): country, state or province, county, city, neighbourhood or postal code, with no data to ship) and sets from any IRegionSource (cfg.AddRegionSet(name, sp => …)). A geocoder set’s region is the placemark, written without any serializer options. Android, iOS and Mac Catalyst. See Region Geofencing.
Feature
app.screen, POST app/store/review-page and GET device/keep-awake. app.screen fires with the new ScreenInfo whenever the main display changes (a rotation, a resolution or scale change, or another display), from Shiny.Core’s IScreenInfo. store/review-page opens the store listing on its reviews, through Shiny.AppStore’s OpenReviewPage. device/keep-awake reports whether the screen is being kept on. All platforms Shiny.Core and Shiny.AppStore support; 501 where there is no store registered.
Fix
Keeping the screen on where the platform cannot is a 501, not a silent success. PUT device/keep-awake reads the value back after setting it. A backend that drops it, such as Essentials on plain .NET, now answers 501 instead of 204. Platforms that implement it are unchanged.
Feature
Database bridge. Shiny.AppDeviceBridge.Database adds /_bridge/database, a database client on the device for SQLite files. It opens SQLite files in the app’s file roots where they are, named by the same { root, path } as the files bridge. A page reads the schema (columns, indexes, foreign keys, checks and triggers). It runs scripts with every result set, per-statement messages, the error’s line, EXPLAIN QUERY PLAN as a tree, and cancel by runId. It pages, sorts, filters, searches, counts and totals a table, and inserts, updates and deletes rows by key. It previews the DDL for a table design, including SQLite’s rebuild, and drops or renames objects. It imports CSV in one transaction and exports a table or a statement’s answer to a file in a root. Query history and saved queries are kept per database in the app’s data directory. Statements are interrupted after DatabaseBridgeOptions.StatementTimeout (30 seconds). Security: queries can write, but SQLite’s authorizer refuses ATTACH (and so VACUUM INTO) and load_extension, so a query cannot reach a file outside the file roots or load native code. Other engines plug in through IDatabaseDriver (services.AddDatabaseDriver<T>()), which serves a connection the page names by { connection, database }; GET database/connections lists them and POST database/databases lists a connection’s databases. Register the bridge with bridge.AddDatabaseBridge(). The client is IDatabaseBridge in Shiny.AppDeviceBridge.Database.Client, or DatabaseBridge in TypeScript. All platforms. See Database.
Feature
Pluggable map, router and geocoder providers, with Azure Maps and Google Maps built in. The maps bridge’s online services are now providers on MapsOptions, the way traffic already was, so the same page draws OpenStreetMap, Azure Maps or Google Maps. o.Basemap takes an IBasemapProvider: ProtomapsBasemapProvider (OpenStreetMap vector tiles, as before), AzureMapsBasemapProvider (Render tiles: road, dark grey, imagery) or GoogleMapsBasemapProvider (Map Tiles API 2D tiles: roadmap, satellite, terrain). Raster maps are served at the new GET /_bridge/maps/basemap/{z}/{x}/{y}, and <BridgeMap> lays them over downloaded regions, which show through offline. o.Directions.Router takes an IRouteProvider: ValhallaRouteProvider, AzureMapsRouteProvider (with live traffic) or GoogleMapsRouteProvider. GET /_bridge/directions reports the router’s onlineModes, a mode it lacks answers 400 mode_unsupported, and every route carries its attribution. AzureMapsGeocoder and GoogleMapsGeocoder join NominatimGeocoder. One AzureMapsCredential (a shared key or Microsoft Entra ID) serves every Azure provider. Keys stay in the app, and each provider can be swapped while the app runs. All platforms. See Providers.
BREAKING
Maps options are providers. MapsOptions.OnlineTiles and OnlineMaxZoom are replaced by o.Basemap = new ProtomapsBasemapProvider(url) { MaxZoom }, whose ConfigureRequest now carries tile headers (MapsOptions.ConfigureRequest covers the catalog, downloads, glyphs and sprites). DirectionsOptions.OnlineRouteUrl, ApiKey and ConfigureRequest are replaced by o.Directions.Router = new ValhallaRouteProvider(url) { ApiKey, ConfigureRequest }. MapsInfo.Online is replaced by MapsInfo.Basemap. TrafficTileFormat is now TileFormat and TrafficTile is now ProviderTile, shared by every tile provider. AzureMapsTrafficProvider(clientId, getAccessToken) becomes AzureMapsTrafficProvider(new AzureMapsCredential(clientId, getAccessToken)).
Feature
Reverse geocoding bridge. AddGeocodingBridge() in Shiny.AppDeviceBridge.Gps adds GET /_bridge/geocoding/reverse?latitude=&longitude=, which returns the placemarks at a position, most relevant first. It uses MapKit / CoreLocation on iOS and Mac Catalyst and android.location.Geocoder on Android. Windows, Linux, macOS and Android devices without a geocoding backend use OpenStreetMap’s Nominatim (Shiny.Gps 5.9.0-beta-0009), so it works on every platform; register AddGeocoding(o => …) first to point it at your own server. A position off the map answers 400, and an unreachable geocoder 503 geocoder_unavailable. The client is IGeocodingBridge in Shiny.AppDeviceBridge.Gps.Client, or GeocodingBridge in TypeScript. See GPS & Motion.
Feature
In-app purchases bridge. Shiny.AppDeviceBridge.InAppPurchases, backed by Shiny.Mobile.InAppPurchases 5.9, adds /_bridge/purchases for consumables, non-consumables and auto-renewable subscriptions through the App Store (StoreKit 2) and Google Play Billing. The page loads localized products with their subscription offers, opens the store’s purchase sheet (Success, Pending, Cancelled and AlreadyOwned are results, not errors), and reads entitlements and unfinished purchases. It can also restore purchases and open subscription management. POST purchases/transactions/{transactionId}/finish with { consume } finishes a purchase once the app’s server has verified verificationData and granted it. The bridge looks the purchase up in the store by its transaction id instead of trusting a record from the page, and answers 404 purchase_not_found when there is none. Ask to Buy approvals, renewals, refunds and purchases on other devices arrive as purchases.updated, in the page or in background.js when no page is open. Store failures answer with codes the page can switch on, such as 503 store_unavailable, 403 not_allowed, 404 product_not_found and 409 invalid_state. Register it with bridge.AddInAppPurchasesBridge(); an app’s own IPurchaseDelegate keeps running beside the bridge’s. The client is IInAppPurchasesBridge in Shiny.AppDeviceBridge.InAppPurchases.Client, or InAppPurchasesBridge in TypeScript. iOS 15+ and Android; every other platform answers 501. See In-App Purchases.
Enhancement
AppDeviceBridgeOptions.DefaultFileRoots. Set it to false and an empty FileRoots gives the page no roots instead of data and cache, so an app whose storage is its own WebAppFileStore (not a directory) can add only that, with WebAppFileRoots.Add, while it runs. On by default; configured FileRoots win either way. All platforms. See Storage.
Enhancement
Shiny 5.9.0-beta-0009. Every Shiny-backed bridge now builds on Shiny 5.9.0-beta-0009, up from beta-0004.
Feature
Printing bridges. Two new packages, one for each of Shiny 5.9’s printing models. Shiny.AppDeviceBridge.Printers adds /_bridge/printers for ESC/POS and TSPL receipt and label printers, backed by Shiny.Printers. It scans Bluetooth LE for known printer models and mDNS for network printers, connects to one at a time, and prints receipts the page builds as a list of elements: text, alignment, size, barcodes, QR codes, images (a PNG or JPEG, scaled to the printer’s width), cuts and raw bytes. POST printers/render turns the same receipt into a PDF. printers.disconnected fires when a printer goes away. A printer named by address must be an IP address on the local network and on a raw printer port (9100–9102, configurable with PrintersBridgeOptions.NetworkPorts), because a receipt can carry raw bytes. Shiny.AppDeviceBridge.Printing adds /_bridge/printing for the operating system’s own printing, backed by Shiny.Printing: print a PDF, an image, HTML or an http(s) page through the print dialog, or silently to an installed printer where the platform allows it. A large PDF or image goes as the raw body of POST printing/print/pdf or print/image (PrintPdfAsync / PrintImageAsync taking a Stream, or printPdf / printImage taking a Blob), streamed to a temporary file, up to PrintingBridgeOptions.MaxFileBytes (512 MB). The JSON route is capped at MaxJsonBytes (16 MB). POST printing/html-to-pdf (HtmlToPdfAsync, or htmlToPdf in TypeScript) lays HTML out on pages and returns the PDF with no print UI, on iOS, Mac Catalyst and Android, where capabilities.htmlToPdf is true; elsewhere it answers 501. Built on Shiny 5.9.0-beta-0009. Register them with bridge.AddPrintersBridge() and bridge.AddPrintingBridge(). The clients are IPrintersBridge (with a PrintElements builder) and IPrintingBridge, or PrintersBridge and PrintingBridge in TypeScript. Network printers work on every platform. Bluetooth LE printers work on Android, iOS, Mac Catalyst, macOS and Windows, and on Linux with Shiny.BluetoothLE.Linux registered. OS printing uses PrintManager on Android, AirPrint on iOS and Mac Catalyst, the spooler on Windows and CUPS on Linux and macOS. Content a platform can’t print, such as HTML on Windows and CUPS, answers 501, as does rendering on Linux without SkiaSharp.NativeAssets.Linux. See Printing.
Feature
Live activities bridge. Shiny.AppDeviceBridge.LiveActivities, backed by Shiny.Mobile.LiveActivities 5.9, adds /_bridge/liveactivities: iOS Live Activities on the Lock Screen and in the Dynamic Island, and Android 16 Live Updates. The page starts, updates and ends activities with a title, body, short status, progress bar, stale date and data for a custom widget. An update can alert instead of refreshing silently. Push tokens, one per activity and the push-to-start token on iOS 17.2+, arrive as liveactivities.token and liveactivities.starttoken, in the page or in background.js when no page is open, so a server can update or start activities over APNs. Register it with bridge.AddLiveActivitiesBridge(), and on iOS set ShinyLiveActivityWidget to build Shiny’s stock widget into the app. The client is ILiveActivitiesBridge in Shiny.AppDeviceBridge.LiveActivities.Client, or LiveActivitiesBridge in TypeScript. iOS 16.2+ and Android; every other platform answers 501. See Live Activities.
BREAKING
Text from a wearable arrives as a string. Shiny.Wearables 5.9 sends text as plain UTF-8 (WearableData.FromString and the string overloads of SendMessage, Transfer and UpdateContext), as does a Swift or Kotlin companion app sending a String. The wearables bridge now delivers bytes that are valid UTF-8 text, but not JSON, to the page as a JSON string with binary: false, in wearables.message, wearables.context and wearables.transfer and in message replies. Before, they arrived as base64 with binary: true. A page that decoded base64 to read a companion app’s text should use data directly. JSON is unchanged, and bytes that are not text still arrive as base64 with binary: true. iOS and Android; every other platform answers 501 as before.
Fix
AppLinks no longer crashes iOS and Mac Catalyst apps that use a scene delegate. When the app launched without a link, the scene-connect handler read link and activity sets that UIKit leaves empty, and threw a NullReferenceException. Apps built with the iOS 27 SDK must use the UIScene life cycle, so every launch of such an app that registered AddAppLinksBridge hit it. Other platforms are unchanged.
BREAKING
Shiny.AppDeviceBridge.Locations is split into Shiny.AppDeviceBridge.Gps and Shiny.AppDeviceBridge.Geofencing, following Shiny.Locations’ split into Shiny.Gps and Shiny.Geofencing. .Gps has AddGpsBridge() and AddMotionActivityBridge(), and .Geofencing has AddGeofenceBridge(). Reference only the one you use. AddLocationBridges() is removed: call AddGpsBridge().AddGeofenceBridge() instead. The clients move with them. Shiny.AppDeviceBridge.Gps.Client has IGpsBridge, IMotionBridge and GpsJsonContext, and Shiny.AppDeviceBridge.Geofencing.Client has IGeofencesBridge and GeofencingJsonContext. LocationsJsonContext is gone. LocationAccessResult becomes GpsAccessResult, MotionAccessResult and GeofenceAccessResult, with the same { "access": … } JSON. In TypeScript, the locations module becomes gps and geofencing, still exported from the package root. Routes, events and handler names are unchanged. Platforms are unchanged: GPS and geofencing on Android, iOS, Mac Catalyst and Windows, motion activity on Android, iOS and Mac Catalyst, and 501 elsewhere.
Feature
Beacons bridge. Shiny.AppDeviceBridge.Beacons, backed by Shiny.Beacons, adds /_bridge/beacons. Ranging reports the iBeacons in a region with estimated distances as beacons.ranged. Monitoring reports region enter and exit in the background and across restarts, as beacons.region and the beacon handler in background.js. Scanning reports Eddystone UID, URL, TLM and EID frames as beacons.eddystone, and broadcasting advertises the device as an iBeacon, Eddystone-UID or Eddystone-URL beacon. Register it with bridge.AddBeaconsBridge(), or pass BeaconFeatures flags for only some features. The client is IBeaconsBridge in Shiny.AppDeviceBridge.Beacons.Client, or BeaconsBridge in TypeScript. Ranging and scans answer 409 not_listening until their event is listened to, and stop with the last listener. Android, iOS, Mac Catalyst and Windows have every feature. macOS has Eddystone and broadcasting, and answers 501 for iBeacon ranging and monitoring, which CoreLocation lacks there. Linux answers 501. See Bridges.