tauri-apps/tauri

Feature: Custom URI scheme support (deep linking)

rihardsgravis opened this issue ยท 62 comments

Custom URL schemes provide a way to reference resources inside an app. Users tapping a custom URL e.g. yourapp://foo?bar in an email, for example, launch the app in a specified context. Other apps can also trigger the app to launch with specific context data; for example, a photo library app might display a specified image.

Tauri could have a simplified API to register/unregister the custom URI scheme protocol a listener.

A Rust package https://github.com/maidsafe/system_uri exists that does System App URI registration for macOS, Linux and Windows. Unfortunately, this package does not support passing also the url params to the application - it only triggers the application launch. An example implementation together with Tauri -https://github.com/iotaledger/spark-wallet/blob/master/src-tauri/src/main.rs

The same goes for the "redirect uri" when authenticating with some external provider that uses Oauth (Auth0 for example).

It'd be nice if there could be something similar to Electron's protocol API which lets you:

  • Register your application to be launched on custom URI schemes, I wonder if there's existing rust packages that support this already.
  • Add "protocol handlers" to the webview so that custom protocol schemes could invoke a function which returns a stream for the HTTP response. I think most webviews have some concept of this
  • Set privileges for certain custom protocols (this is probably hardest to do with public OS WebView APIs)

Regarding the protocol handlers, this is something that would need to be done in the webview dependency.

In the dev branch of Tauri, we already use custom protocols to load web assets (See tauri-apps/wry#65). However, you can't create your own, is this a feature that would be useful?

@nklayman It'd be super useful for me! I'm working on a p2p web browser called Agregore which uses Electron's protocol handlers to support loading content from p2p protocols like IPFS and Hyprcore-protcol as well as indie protocols like Gemini.

I like electron, but it'd be nice to be able to go closer to the metal and ditch all the JavaScript that's on the backend and use Rust. ๐Ÿ˜

The other thing which I think is missing in a lot of webview libraries is the registerScemesAsPriviledged API that Electron provides.

This is what lets custom protocol handlers to access APIs which are normally only available via HTTPs as well as make the URL parser treat them as "proper" URLs so that they can get a correct hostname set rather than everything going into the pathname. Not sure if this is possible with Tauri, though.

As a matter of fact, this is what wry is actually doing with its custom scheme @RangerMauve see:

https://github.com/tauri-apps/wry/blob/c49846cfc41bb548a685edeac5f8036501f7dcec/src/webview/mimetype.rs#L115

Oh wow! That's really cool to see @nothingismagick

It'd be super handy if wry provided APIs to register different protocol schemes like that.
I'd love to ditch all the extra bloat from Electron + Node!

#1553 is related to some of what has been discussed here (although that's not related to the original issue, which is just about deeplink support).

Hello, I want to know how this function is progressing.
I am using the dev branch of tauri and wry, but the registered protocol is still unavailable.
Failed to launch'tes://az' because the scheme does not have a registered handler.
For some applications that use oauth, this is a very convenient feature.

+1 or could someone show an example on how to use it?

The work on this feature is frozen because we're focusing on bug fixes ahead of a stable release and our audit.

@lucasfernog Thanks for the update! Is this something you'll be able to continue on after the release/audit?

@lucasfernog Thanks for the update! Is this something you'll be able to continue on after the release/audit?

Yeah hopefully I can get back to it after the release.

Sweet, I'm excited to see how it progresses. ๐Ÿ˜ This functionality can enable a lot of interesting use cases (especially for people making experimental browsers like myself).

@lucasfernog Sorry to push you on this but its a feature we need it ASAP for us to use with oath. Also please consider how to communicate the change in deeplink to sidecar

@hellomocks - we are currently in a feature freeze while the audit is ongoing and have some important revision work to do, so as Lucas said, this is not something that the core team will be getting to before the end of September.

@hellomocks - we are currently in a feature freeze while the audit is ongoing and have some important revision work to do, so as Lucas said, this is not something that the core team will be getting to before the end of September.

I would add, the custom URI support is handled by the registry keys on Windows and on macOS it's handled by a custom PLIST;

something like this may work;

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>CustomID</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>tauriapp</string>
    </array>
  </dict>
</array>

Tauri supports custom plist on macOS, for Windows registry key they can probably be done through a custom WIX template or you can do it in rust when the app launch for the first time. Linux with some research it can be done easily, but it depends how you package your app.

or you can do it in rust when the app launch for the first time. Linux with some research it can be done easily, but it depends how you package your app.

The initial poster mentioned implementing this via the system_uri package and @Shot on Discord implemented a forked version of that crate. I tested it out as well using the mentioned library but with just the registry keys the default command will just keep opening new instances of the application instead of reusing the existing instance.

Are you aware of a way to intercept the triggers or to redirect the invocation to an existing window instead of opening a second instance?

This can be achieved with WIX but currently don't understand how to get the context replacement working with fragments. However, the wix settings allow you to override the default template which can be modified to achieve this effort.

<Component Id="ApplicationURI" Guid="*">
                <RegistryKey Root="HKCR" Key="acme">
                    <RegistryValue Type="string" Value="acme" />
                    <RegistryValue Name="URL Protocol" Type="string" Value=""/>
                    <RegistryKey Key="shell">
                        <RegistryKey Key="open">
                            <RegistryKey Key="command">
                                <RegistryValue Type="string" Value='{{{app_exe_source}}} "%1"'/>
                            </RegistryKey>
                        </RegistryKey>
                    </RegistryKey>
                </RegistryKey>
            </Component>

I placed this component in the

 <DirectoryRef Id="INSTALLDIR">

section of the default WIX template (https://github.com/tauri-apps/tauri/blob/b0a8c38a736544cdd70fd10155e5ad3a25c81535/tooling/bundler/src/bundle/windows/templates/main.wxs)

Then I updated my tauri.conf.json bundle section to look the following:

"windows": {
        "certificateThumbprint": null,
        "digestAlgorithm": "sha256",
        "timestampUrl": "",
        "wix":{
          "template":"wixFragments/template.wxs" // <-- wixFragments is a folder in my src-tauri parent directory
        }
      }

This will create the registry keys under Computer\HKEY_CLASSES_ROOT\acme\shell\open\command which is where the file associates/commands live to be found for invocation. This will allow acme:// to open the application. I will caveat that this approach will launch a new instance every time it encounters the scheme. It will not re-use an existing instance and pass that information into the instance. I am still digging into that bit of work as well as how to make this part of a fragment instead of a custom template.

My list of items to continue investigating:

  • How to make it re-use an instance
  • How to invoke a command when a scheme is invoked with parameters
  • File associations (which is registry driven like custom uris)

has there been any updates on this and if there are, is there an example?

@RiChrisMurphy No, otherwise it would be listed or linked here...

This seems like it would be useful for redirect uri for some OAuth2 flows as @system32uwu mentioned. Currently is there anyway to handle 3rd party OAuth authentication from a Tauri APP ?

Hello @nothingismagick, any update on this ?

This is all that's left for me before I make the switch from Electron. Woohoo! Can't wait for someone much smarter and capable than myself to PR this.

Please, can people stop posting messages like "are there any updates" or comments in the "+1" spirit? FabianLars already said:

No, otherwise it would be listed or linked here...

A lot of people are subscribed to this issue since it is a well wanted feature. And everytime those people get a notification about these comments. It's just annoying :) With that being said, can these kind of comments be marked as off-topic, please? Including mine.

Thank you.

This seems like it would be useful for redirect uri for some OAuth2 flows as system32uwu mentioned. Currently is there anyway to handle 3rd party OAuth authentication from a Tauri APP ?

@vikigenius I know it's been a long time since this was posted but I thought maybe my old, undocumented, probably suboptimal code might be of use to people trying to implement authentication using 3rd party providers in tauri:
https://github.com/ohmree/stfu-old
I don't remember exactly what I did there off the top of my head but I do remember having a functioning prototype, and there isn't that much code to read through so I believe anyone should be able to borrow the method I came up with (unless some major update somehow broke it while I was on a break from fiddling with tauri).
Anyways I think I could help adapt this to other codebases if anyone's interested and can't figure it out on their own just from reading through the code, or maybe even contribute an example (but I might need some help with that).

Hey everyone,

as I'm also waiting for updates for this, I kind of managed to come up with a workaround (more like a hackaround) for now to handle OAuth.

  1. Create a new window to handle OAuth.
  2. Create a Tauri command which checks whether the OAuth redirect was successful. If it is, redirect to the local app path to handle the success(e.g. tauri.localhost/oauth/success). I've done this with evaluating Javascript inside of Rust.
  3. In the source window, set an interval to invoke this command after the OAuth flow is initiated.

Hope this helps! ๐Ÿฅณ

@elvinaspredkelis that's certainly useful, I have finished my project half a year ago, but if you could provide a working example I'd be happy to port it from Qt to Tauri.

Something similar to what you propose I did with electron in another project, here. I just waited for the second window to redirect to the approved url and to contain "#access_token=", then split the string, grab the token and store it in a local config file, but I could've also replied to the event with the token which I think would've been better.

This would require Tauri to add events to the core library for windows to communicate between themselves (not sure if this is already implemented or not).

@loicortola - thanks for bringing this up again, indeed we forgot about it. I spoke with @lucasfernog briefly this afternoon, and while it should be ok to make work, the devil (and the security auditing needed) will be in the details.

As is mentioned here, I've done a solution for Windows by:

  1. using customized wix for registering a deep link and make it as a --deep-link parameter
  2. using the tauri's cli api for parsing that deep link, also the parsed matters can be invoked by the frontend
  3. dealing with the parsed matters in the frontend to whatever I want to make it.

and the security auditing needed

Definitely worth checking all the security holes Electron fixed over the years with their URL scheme feature!

I see incredible opportunities if we can get deep linking working. Will follow this thread. Thanks to everyone building this tremendous tool. ๐Ÿ™๐Ÿผ

Hi curious to know if there have been any updates on this. Thanks!

@hariria #323 (comment)

As far as i am concerned, apart from the security stuff mentioned earlier another big blocker to even start implementing it is macos support. macos is the only platform that needs changes in the windowing lib (currently tao). And since we want to merge back to winit we're mainly blocked by this issue i think rust-windowing/winit#2120

@FabianLars
Is it possible to implement only for Windows ?

@yuszafar Yes, see this comment #323 (comment) for a solution to register it at install time or my explorations here https://github.com/FabianLars/tauri-plugin-deep-link if registering it on app start works for you

FWIW, we are in the process of validating Tauri for us to rewrite our legacy Desktop App and this is one of the only blocker at this point. So very much looking forward to this feature being implemented.

Thanks!

Could we use this? https://docs.rs/tauri/1.2.0/tauri/struct.Builder.html#method.register_uri_scheme_protocol

something like the following:

fn main() {

  tauri::Builder::default()
          .invoke_handler(tauri::generate_handler![greet])
          .register_uri_scheme_protocol("example", move |app, request| {
             ...something here
          })
          .run(tauri::generate_context!())
          .expect("error while running tauri application");
}

@eli-front

Could we use this? https://docs.rs/tauri/1.2.0/tauri/struct.Builder.html#method.register_uri_scheme_protocol

No, that's something completely different. This only registers an app-specific scheme (think of an alternative to http:) - this is only accessible inside your app, not elsewhere.

@nothingismagick, any status update or thought on this one? Is anybody assigned or actively looking into this? We're still ramping up with Tauri/Rust and everything, but this is a key feature for us. How can we best help?

Thanks!

@gegles My comment above #323 (comment) should still be accurate. afaik our main concern here still is winit's implementation because we don't want to diverge too much from it at this point.
Also the huge security implications probably prevent us adding it in a 1.X release anyway. I'm not one of our security gurus so take that with a grain of salt.

Apart from that, proof-of-concepts of various forms already exist so i don't think there's much you can do to help us (other than take part of the winit discussions linked above)

Thanks @FabianLars! We're not in a super rush as we're just at the beginning of rewriting our app with Tauri, so hopefully, by the time we get there, it'll be available.

In the meantime, do you know of any plugins/impls that get you this feature somewhat? Like on macOS and Windows primarily. Thanks!

For windows i added some notes here already: #323 (comment) - the plugin i linked should work on Linux too, but there might be an issue with how it sets the binary path, i really need to check that when i have the time.

macOS is the only problematic thing here again, it's the only platform where this can't be added on-top and needs changes in tao/winit because of Apple's interesting design choices.

macOS is the only problematic thing here again, it's the only platform where this can't be added on-top and needs changes in tao/winit because of Apple's interesting design choices.

@FabianLars, I am still very new to Rust/Tauri, so this may be completely dumb, but could the fruitbasket crate and specifically the example here, be leveraged, at least as a workaround in your tauri-plugin-deep-link to get the macOS functionality?

Happy to help building this if you think it is viable.

@gegles while i was never able to make fruitbasket work in my app (on my stupid vm) it still ended up being a valuable resource in creating something that does seem to work for me. In other words, the plugin should support macos now too.

@gegles while i was never able to make fruitbasket work in my app (on my stupid vm) it still ended up being a valuable resource in creating something that does seem to work for me. In other words, the plugin should support macos now too.

@FabianLars, thank you so much! I've been following your PR closely, and I've now put someone from my team on this to see if everything works for us. We'll let you know, but so appreciate all your work already!

@FabianLars Been using the plugin on Windows 10/11 for a few months now, and it's been working flawlessly. Attempting to port to Mac at the moment - could you clarify what changes need to be made to the Tauri config? I'm just running the generated default config, but it's not registering the scheme in development + release mode. AFAIK there isn't a way to register deep links in develop mode, right?

AFAIK there isn't a way to register deep links in develop mode, right?

Correct, there used to be deprecated apis for that but they stopped working in the latest macos versions so i didn't even bother adding them.

Basically you need to completely build the app (either .app or .dmg) and install it, running just the binary is not enough. Some resources said that you sometimes have to restart/logout for it to take effect but it worked for me without that.
Oh and, don't forget to add and configure the Info.plist file next to your tauri.conf.json file, that's what actually registers the schemes.

p.s. once you have an app installed you can use tauri dev again (close the installed app first) and it should redirect requests to the dev instance but that's a bit wonky so i may remove it in the future.

Soooo, the tldr is:

  • Copy the example Info.plist file next to your tauri config and change it according to your needs (this is really important)
  • add prepare at the top, and call register in the setup hook like in the example main.rs file. The scheme in the register function will be ignored on macos.
  • Run tauri build and install the app. Maybe restart the system.

Edit: One thing i forgot, the emit_all call in the setup hook is a bit stupid on macos, if your app gets launched by the uri scheme, because the js listeners won't be registered at that point.

FYI there's some caveats about what programs are allowed to take focus on Windows. Normally when implementing single instance deep linking you call AllowSetForegroundWindow to give another app permission to take foreground focus. Otherwise, it may just blink helplessly on the taskbar. But when that protocol activation comes from a Windows notification, that call fails with ERROR_ACCESS_DENIED because one of a list of conditions must be met for the call to succeed.

Chromium works around this by sending a dummy key press event, which lets AllowSetForegroundWindow succeed.

@FabianLars 's tauri-plugin-deep-link works pretty well when launching via Start -> Run, but was suffering from the same focus issue when launched from a Windows Notification. It may be that implementing a Chrome-style workaround is what's required for that scenario. The workaround functioned perfectly for me in Electron.

@RandomEngy This sounds like a general issue not just related to custom schemes, right? If so please open a separate issue for this! (preferably with a minimal reproduction app)

p.s. does tauri's window.set_focus() api work as a user-side workaround?

This issue happens when a second instance is launched from Windows Notifications. A protocol launch is one way for this to happen. I brought it up here because any implementation that tries to activate the primary app instance should be aware of this. I'll get together a small repro app using tauri-plugin-deep-link that demonstrates this. But I wouldn't say it's a bug on Tauri itself right now; there's no built-in feature that tries to activate a primary instance from a secondary one.

The call to window.set_focus() is part of the repro steps:

  1. Set up tauri_plugin_deep_link::register with a handler that tries to call window.set_focus()
  2. Open the app
  3. Fire a notification that opens the app with a protocol
  4. Put the app in the background by focusing a different app
  5. Click the notification

Expected result:
App comes to the foreground

Actual result:
App flashes in the taskbar and does not come to the foreground

Edit: One thing i forgot, the emit_all call in the setup hook is a bit stupid on macos, if your app gets launched by the uri scheme, because the js listeners won't be registered at that point.

@FabianLars Hi, I'm using tauri-plugin-deep-link on Windows only
It seems the js listeners also not run on Windows if the app gets launched by the uri scheme
I don't know much of rust, just literaly copy the code in example, does it expected to work?

also, the url scheme could launch the app, but seems cannot focus the app if already launched (the listeners gets called, just the window does not get focused), is it an unsolved issue?

@joshua7v

It seems the js listeners also not run on Windows if the app gets launched by the uri scheme
I don't know much of rust, just literaly copy the code in example, does it expected to work?

This is expected behavior. There is a comment and a small example in the repo example showing how to read the url if the primary instance was started via the scheme.

also, the url scheme could launch the app, but seems cannot focus the app if already launched (the listeners gets called, just the window does not get focused), is it an unsolved issue?

That kinda depends on your code i think, you may have to call window.set_focus() (should also work in js), if that doesn't work, please open an issue at my repo and we'll take a look.

@FabianLars

Thanks for the helpful guidance, I did miss the comment under example code, however the listeners still don't get invoked after putting the code in. The set_focus() works well

I have fired an new issue at the deep link repo, hope you could easily reproduce it.

@FabianLars So it seems that deep linking on different OS comes with different set of problems.. Do you know whether deep linking for Tauri Mobile will come with an extra set of complications?

The link handling on iOS seems to be covered by the macos implementation already. Registering the links are similar but doesn't re-use the same plist file (since it's not an .app bundle) afaik.

The android implementation looks like it can't reuse anything. I don't know how hard that will be since I didn't really look into it yet but I'd imagine it also has some annoying quirks like all the other platforms.

(P.S. either way I'm happy that I don't have to handle that in the plugin :D )

The same goes for the "redirect uri" when authenticating with some external provider that uses Oauth (Auth0 for example).

How you bypass this,please?

on macOS, you can use ASWebAuthenticationSession.
That's not for deeplinking, just for Authentication though

rr13k commented

I am very concerned about this issue and look forward to new progress

After spending some time trying to figure out why tauri-plugin-deep-link stopped working in alpha.14 on Mac, I decided to share my findings:

The way the plugin gathers the URLs is through registering a handler on NSAppleEventManager . This is the OLD way. The new way is to use application:getURLS. And interestingly enough, this is what TAO already does (starting with this commit), but from what I understand not all TAO events are available in Tauri. What I discovered from a random person on the internet is that once you register the event handler (the old way) the method on the delegate (the new way) will not be called anymore. Which led me to an idea, that maybe the NSAppleEventManager was being called too early. I added an arbitrary 5 second delay before registering the event handler and it worked (and unsurprisingly, judgning from the lack of the relevant trace level log messages, the getURLs method stopped being called).

All of this leads me to a conclusion that for Tauri v2, the best way to implement custom scheme support on Mac OS is to propagate the event triggered by getURLs all the way to Tauri and use it instead of the NSAppleEventHandler.

Hope my discovery session aids someone.