The parts of the installed-app story that are declared in the manifest rather than called from C# - share targets, protocol handlers and related apps - and where each of them hands control back to Butil.
Note:
Manifest first, code second
Everything on this page starts in manifest.webmanifest. The manifest is what makes
the browser offer an install, register the app for a file type or a scheme, and put it in the
system share sheet; the C# below is only what happens after the OS has routed something to
your app.
Web Share Target - the receiving half
share_target
Butil's Navigator.Share sends; Web Share Target receives. There is no JavaScript API for it at all: an installed app declares a share_target in its manifest and the OS then lists it in the system share sheet. A GET target arrives as an ordinary navigation with query parameters; a POST target arrives as a multipart form, which needs a service worker to intercept - a Blazor client cannot read a POST body the browser posted to it.
With method GET the share is just a navigation to your action URL, so the shared data is in the query string and any Blazor page can read it. This is the one share-target shape worth reaching for first.
{"name":"Bit.Butil demo","start_url":"/","display":"standalone","icons":[{"src":"/icon-192.png","sizes":"192x192","type":"image/png"}],"//":"The params names are the keys the OS fills in; the query string the app reads uses them.","share_target":{"action":"/share","method":"GET","params":{"title":"title","text":"text","url":"url"}}}
Receiving shared files
share_target + LaunchQueue
A share that carries files must be a POST target with enctype multipart/form-data, and the files reach the app through a service worker that intercepts the request. Where the browser routes the share as a launch instead, LaunchQueue.SetConsumer is what receives it - the same handler that serves file_handlers.
// A Blazor client cannot read a body the browser POSTed to it, so the worker takes the request// apart before the page ever loads and answers with a redirect to a URL the page can act on.
self.addEventListener('fetch', event =>{const url =newURL(event.request.url);if(event.request.method !=='POST'|| url.pathname !=='/share')return;
event.respondWith((async()=>{const form =await event.request.formData();const files = form.getAll('media');// The files only exist for this request, so they are parked where the page can reach them.const cache =await caches.open('shared-media');awaitPromise.all(files.map((file, index)=>
cache.put(`/shared-media/${index}`,newResponse(file,{
headers:{'content-type': file.type,'x-filename': file.name }}))));// 303 so the browser re-issues it as a GET: the app starts on an ordinary navigation.returnResponse.redirect(`/share?count=${files.length}`,303);})());});
@page"/share"@injectBit.Butil.CacheStorage cacheStorage
@code {// The worker has already stashed the files by the time this page runs; the query string only// says how many to collect.privateasyncTaskLoadShared(int count){for(var index =0; index < count; index++){var cached =await cacheStorage.Match("shared-media",$"/shared-media/{index}");if(cached.Found)Open(cached.Headers["x-filename"], cached.Body);}}}
Custom URL schemes
Navigator.RegisterProtocolHandler
Offers this site as the handler for a URL scheme, so a web+butil: link brings the user here. The scheme must be safelisted (mailto, tel, sms, webcal…) or prefixed with web+, and the url must be same-origin and contain a single %s placeholder. Nothing observable happens on success - the browser asks the user, in its own time.
C#
@inject Bit.Butil.Navigator navigator
var registered =await navigator.RegisterProtocolHandler("web+butil","/open?link=%s");
Live sample
protocol handler output
Results will appear here when you interact with the samples.
Is the native app already installed?
Navigator.GetInstalledRelatedApps
Reports which of the apps your manifest claims in related_applications are actually installed - the check behind 'you already have our app, open it there'. Deliberately not an install enumerator: an app the manifest doesn't claim is never reported, and the relationship has to be proven from the other side too.
C#
var installed =await navigator.GetInstalledRelatedApps();if(installed.Any(app => app.Platform=="play")){// don't offer the web install - they already have the Android app}
Live sample
related apps output
Results will appear here when you interact with the samples.
Where the rest lives
The three parts of this story that are real APIs each have their own page: InstallPrompt for the install button, LaunchQueue for files and launch URLs arriving from the OS, and WindowControlsOverlay for a desktop app that draws its own title bar.