664 lines
18 KiB
Text
664 lines
18 KiB
Text
---
|
|
title: Custom URL Protocols
|
|
description: Register custom URL schemes to launch your application from links
|
|
sidebar:
|
|
order: 3
|
|
---
|
|
|
|
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
|
|
|
Custom URL protocols (also called URL schemes) allow your application to be launched when users click links with your custom protocol, such as `myapp://action` or `myapp://open/document`.
|
|
|
|
## Overview
|
|
|
|
Custom protocols enable:
|
|
- **Deep linking**: Launch your app with specific data
|
|
- **Browser integration**: Handle links from web pages
|
|
- **Email links**: Open your app from email clients
|
|
- **Inter-app communication**: Launch from other applications
|
|
|
|
**Example**: `myapp://open/document?id=123` launches your app and opens document 123.
|
|
|
|
## Configuration
|
|
|
|
Define custom protocols in your application options:
|
|
|
|
```go
|
|
package main
|
|
|
|
import "github.com/wailsapp/wails/v3/pkg/application"
|
|
|
|
func main() {
|
|
app := application.New(application.Options{
|
|
Name: "My Application",
|
|
Description: "My awesome application",
|
|
Protocols: []application.Protocol{
|
|
{
|
|
Scheme: "myapp",
|
|
Description: "My Application Protocol",
|
|
Role: "Editor", // macOS only
|
|
},
|
|
},
|
|
})
|
|
|
|
// Register handler for protocol events
|
|
app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
|
|
url := event.Context().ClickedURL()
|
|
handleCustomURL(url)
|
|
})
|
|
|
|
app.Run()
|
|
}
|
|
|
|
func handleCustomURL(url string) {
|
|
// Parse and handle the custom URL
|
|
// Example: myapp://open/document?id=123
|
|
println("Received URL:", url)
|
|
}
|
|
```
|
|
|
|
## Protocol Handler
|
|
|
|
Listen for protocol events to handle incoming URLs:
|
|
|
|
```go
|
|
app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
|
|
url := event.Context().ClickedURL()
|
|
|
|
// Parse the URL
|
|
parsedURL, err := parseCustomURL(url)
|
|
if err != nil {
|
|
app.Logger.Error("Failed to parse URL:", err)
|
|
return
|
|
}
|
|
|
|
// Handle different actions
|
|
switch parsedURL.Action {
|
|
case "open":
|
|
openDocument(parsedURL.DocumentID)
|
|
case "settings":
|
|
showSettings()
|
|
case "user":
|
|
showUser Profile(parsedURL.UserID)
|
|
default:
|
|
app.Logger.Warn("Unknown action:", parsedURL.Action)
|
|
}
|
|
})
|
|
```
|
|
|
|
## URL Structure
|
|
|
|
Design clear, hierarchical URL structures:
|
|
|
|
```
|
|
myapp://action/resource?param=value
|
|
|
|
Examples:
|
|
myapp://open/document?id=123
|
|
myapp://settings/theme?mode=dark
|
|
myapp://user/profile?username=john
|
|
```
|
|
|
|
**Best practices:**
|
|
- Use lowercase scheme names
|
|
- Keep schemes short and memorable
|
|
- Use hierarchical paths for resources
|
|
- Include query parameters for optional data
|
|
- URL-encode special characters
|
|
|
|
## Platform Registration
|
|
|
|
Custom protocols are registered differently on each platform.
|
|
|
|
<Tabs syncKey="platform">
|
|
<TabItem label="Windows" icon="seti:windows">
|
|
|
|
### Windows NSIS Installer
|
|
|
|
**Wails v3 automatically registers custom protocols** when using NSIS installers.
|
|
|
|
#### Automatic Registration
|
|
|
|
When you build your application with `wails3 build`, the NSIS installer:
|
|
1. Automatically registers all protocols defined in `application.Options.Protocols`
|
|
2. Associates protocols with your application executable
|
|
3. Sets up proper registry entries
|
|
4. Removes protocol associations during uninstall
|
|
|
|
**No additional configuration required!**
|
|
|
|
#### How It Works
|
|
|
|
The NSIS template includes built-in macros:
|
|
- `wails.associateCustomProtocols` - Registers protocols during installation
|
|
- `wails.unassociateCustomProtocols` - Removes protocols during uninstall
|
|
|
|
These macros are automatically called based on your `Protocols` configuration.
|
|
|
|
#### Manual Registry (Advanced)
|
|
|
|
If you need manual registration (outside NSIS):
|
|
|
|
```batch
|
|
@echo off
|
|
REM Register custom protocol
|
|
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /ve /d "URL:My Application Protocol" /f
|
|
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /v "URL Protocol" /t REG_SZ /d "" /f
|
|
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp\shell\open\command" /ve /d "\"%1\"" /f
|
|
```
|
|
|
|
#### Testing
|
|
|
|
Test your protocol registration:
|
|
|
|
```powershell
|
|
# Open protocol URL from PowerShell
|
|
Start-Process "myapp://test/action"
|
|
|
|
# Or from command prompt
|
|
start myapp://test/action
|
|
```
|
|
|
|
### Windows MSIX Package
|
|
|
|
Custom protocols are also automatically registered when using MSIX packaging.
|
|
|
|
#### Automatic Registration
|
|
|
|
When you build your application with MSIX, the manifest automatically includes protocol registrations from your `build/config.yml` protocols configuration.
|
|
|
|
The generated manifest includes:
|
|
|
|
```xml
|
|
<uap:Extension Category="windows.protocol">
|
|
<uap:Protocol Name="myapp">
|
|
<uap:DisplayName>My Application Protocol</uap:DisplayName>
|
|
</uap:Protocol>
|
|
</uap:Extension>
|
|
```
|
|
|
|
#### Universal Links (Web-to-App Linking)
|
|
|
|
Windows supports **Web-to-App linking**, which works similarly to Universal Links on macOS. When deploying your application as an MSIX package, you can enable HTTPS links to launch your app directly.
|
|
|
|
<Aside type="note">
|
|
Web-to-App linking requires manual manifest configuration. Custom protocol schemes are automatically configured from `build/config.yml`, but associated domains must be added manually to your MSIX manifest.
|
|
</Aside>
|
|
|
|
To enable Web-to-App linking, follow the [Microsoft guide on web-to-app linking](https://learn.microsoft.com/en-us/windows/apps/develop/launch/web-to-app-linking). You'll need to:
|
|
|
|
1. **Manually add App URI Handler to your MSIX manifest** (`build/windows/msix/app_manifest.xml`):
|
|
```xml
|
|
<uap3:Extension Category="windows.appUriHandler">
|
|
<uap3:AppUriHandler>
|
|
<uap3:Host Name="myawesomeapp.com"/>
|
|
</uap3:AppUriHandler>
|
|
</uap3:Extension>
|
|
```
|
|
|
|
2. **Configure `windows-app-web-link` on your website:** Host a `windows-app-web-link` file at `https://myawesomeapp.com/.well-known/windows-app-web-link`. This file should contain your app's package information and the paths it handles.
|
|
|
|
When a Web-to-App link launches your application, you'll receive the same `ApplicationOpenedWithURL` event as with custom protocol schemes.
|
|
|
|
</TabItem>
|
|
|
|
<TabItem label="macOS" icon="apple">
|
|
|
|
### Info.plist Configuration
|
|
|
|
On macOS, protocols are registered via your `Info.plist` file.
|
|
|
|
#### Automatic Configuration
|
|
|
|
Wails automatically generates the `Info.plist` with your protocols when you build with `wails3 build`.
|
|
|
|
The protocols from `application.Options.Protocols` are added to:
|
|
|
|
```xml
|
|
<key>CFBundleURLTypes</key>
|
|
<array>
|
|
<dict>
|
|
<key>CFBundleURLName</key>
|
|
<string>My Application Protocol</string>
|
|
<key>CFBundleURLSchemes</key>
|
|
<array>
|
|
<string>myapp</string>
|
|
</array>
|
|
<key>CFBundleTypeRole</key>
|
|
<string>Editor</string>
|
|
</dict>
|
|
</array>
|
|
```
|
|
|
|
#### Testing
|
|
|
|
```bash
|
|
# Open protocol URL from terminal
|
|
open "myapp://test/action"
|
|
|
|
# Check registered handlers
|
|
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep myapp
|
|
```
|
|
|
|
#### Universal Links
|
|
|
|
In addition to custom protocol schemes, macOS also supports **Universal Links**, which allow your app to be launched by regular HTTPS links (e.g., `https://myawesomeapp.com/path`). Universal Links provide a seamless user experience between your web and desktop app.
|
|
|
|
<Aside type="caution">
|
|
Universal Links require your macOS app to be **code-signed** with a valid Apple Developer certificate and provisioning profile. Unsigned or ad-hoc signed builds will not be able to open Universal Links. Ensure your app is properly signed before testing.
|
|
</Aside>
|
|
|
|
To enable Universal Links, follow the [Apple guide on supporting Universal Links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app). You'll need to:
|
|
|
|
1. **Add entitlements** in your `entitlements.plist`:
|
|
```xml
|
|
<key>com.apple.developer.associated-domains</key>
|
|
<array>
|
|
<string>applinks:myawesomeapp.com</string>
|
|
</array>
|
|
```
|
|
|
|
2. **Add NSUserActivityTypes to Info.plist**:
|
|
```xml
|
|
<key>NSUserActivityTypes</key>
|
|
<array>
|
|
<string>NSUserActivityTypeBrowsingWeb</string>
|
|
</array>
|
|
```
|
|
|
|
3. **Configure `apple-app-site-association` on your website:** Host an `apple-app-site-association` file at `https://myawesomeapp.com/.well-known/apple-app-site-association`.
|
|
|
|
When a Universal Link triggers your app, you'll receive the same `ApplicationOpenedWithURL` event, making the handling code identical to custom protocol schemes.
|
|
|
|
</TabItem>
|
|
|
|
<TabItem label="Linux" icon="linux">
|
|
|
|
### Desktop Entry
|
|
|
|
On Linux, protocols are registered via `.desktop` files.
|
|
|
|
#### Automatic Configuration
|
|
|
|
Wails generates a desktop entry file with protocol handlers when you build with `wails3 build`.
|
|
|
|
**Fixed in v3**: Linux desktop template now properly includes protocol handling.
|
|
|
|
The generated desktop file includes:
|
|
|
|
```ini
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=My Application
|
|
Exec=/usr/bin/myapp %u
|
|
MimeType=x-scheme-handler/myapp;
|
|
```
|
|
|
|
#### Manual Registration
|
|
|
|
If needed, manually install the desktop file:
|
|
|
|
```bash
|
|
# Copy desktop file
|
|
cp myapp.desktop ~/.local/share/applications/
|
|
|
|
# Update desktop database
|
|
update-desktop-database ~/.local/share/applications/
|
|
|
|
# Register protocol handler
|
|
xdg-mime default myapp.desktop x-scheme-handler/myapp
|
|
```
|
|
|
|
#### Testing
|
|
|
|
```bash
|
|
# Open protocol URL
|
|
xdg-open "myapp://test/action"
|
|
|
|
# Check registered handler
|
|
xdg-mime query default x-scheme-handler/myapp
|
|
```
|
|
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
## Complete Example
|
|
|
|
Here's a complete example handling multiple protocol actions:
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
type App struct {
|
|
app *application.Application
|
|
window *application.WebviewWindow
|
|
}
|
|
|
|
func main() {
|
|
app := application.New(application.Options{
|
|
Name: "DeepLink Demo",
|
|
Description: "Custom protocol demonstration",
|
|
Protocols: []application.Protocol{
|
|
{
|
|
Scheme: "deeplink",
|
|
Description: "DeepLink Demo Protocol",
|
|
Role: "Editor",
|
|
},
|
|
},
|
|
})
|
|
|
|
myApp := &App{app: app}
|
|
myApp.setup()
|
|
|
|
app.Run()
|
|
}
|
|
|
|
func (a *App) setup() {
|
|
// Create window
|
|
a.window = a.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
Title: "DeepLink Demo",
|
|
Width: 800,
|
|
Height: 600,
|
|
URL: "http://wails.localhost/",
|
|
})
|
|
|
|
// Handle custom protocol URLs
|
|
a.app.Event.On(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
|
|
customURL := event.Context().ClickedURL()
|
|
a.handleDeepLink(customURL)
|
|
})
|
|
}
|
|
|
|
func (a *App) handleDeepLink(rawURL string) {
|
|
// Parse URL
|
|
parsedURL, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
a.app.Logger.Error("Failed to parse URL:", err)
|
|
return
|
|
}
|
|
|
|
// Bring window to front
|
|
a.window.Show()
|
|
a.window.SetFocus()
|
|
|
|
// Extract path and query
|
|
path := strings.Trim(parsedURL.Path, "/")
|
|
query := parsedURL.Query()
|
|
|
|
// Handle different actions
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) == 0 {
|
|
return
|
|
}
|
|
|
|
action := parts[0]
|
|
|
|
switch action {
|
|
case "open":
|
|
if len(parts) >= 2 {
|
|
resource := parts[1]
|
|
id := query.Get("id")
|
|
a.openResource(resource, id)
|
|
}
|
|
|
|
case "settings":
|
|
section := ""
|
|
if len(parts) >= 2 {
|
|
section = parts[1]
|
|
}
|
|
a.openSettings(section)
|
|
|
|
case "user":
|
|
if len(parts) >= 2 {
|
|
username := parts[1]
|
|
a.openUserProfile(username)
|
|
}
|
|
|
|
default:
|
|
a.app.Logger.Warn("Unknown action:", action)
|
|
}
|
|
}
|
|
|
|
func (a *App) openResource(resourceType, id string) {
|
|
fmt.Printf("Opening %s with ID: %s\n", resourceType, id)
|
|
// Emit event to frontend
|
|
a.app.Event.Emit("navigate", map[string]string{
|
|
"type": resourceType,
|
|
"id": id,
|
|
})
|
|
}
|
|
|
|
func (a *App) openSettings(section string) {
|
|
fmt.Printf("Opening settings section: %s\n", section)
|
|
a.app.Event.Emit("navigate", map[string]string{
|
|
"page": "settings",
|
|
"section": section,
|
|
})
|
|
}
|
|
|
|
func (a *App) openUserProfile(username string) {
|
|
fmt.Printf("Opening user profile: %s\n", username)
|
|
a.app.Event.Emit("navigate", map[string]string{
|
|
"page": "user",
|
|
"user": username,
|
|
})
|
|
}
|
|
```
|
|
|
|
## Frontend Integration
|
|
|
|
Handle navigation events in your frontend:
|
|
|
|
```javascript
|
|
import { Events } from '@wailsio/runtime'
|
|
|
|
// Listen for navigation events from protocol handler
|
|
Events.On('navigate', (event) => {
|
|
const { type, id, page, section, user } = event.data
|
|
|
|
if (type === 'document') {
|
|
// Open document with ID
|
|
router.push(`/document/${id}`)
|
|
} else if (page === 'settings') {
|
|
// Open settings
|
|
router.push(`/settings/${section}`)
|
|
} else if (page === 'user') {
|
|
// Open user profile
|
|
router.push(`/user/${user}`)
|
|
}
|
|
})
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
### Validate All Input
|
|
|
|
Always validate and sanitize URLs from external sources:
|
|
|
|
```go
|
|
func (a *App) handleDeepLink(rawURL string) {
|
|
// Parse URL
|
|
parsedURL, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
a.app.Logger.Error("Invalid URL:", err)
|
|
return
|
|
}
|
|
|
|
// Validate scheme
|
|
if parsedURL.Scheme != "myapp" {
|
|
a.app.Logger.Warn("Invalid scheme:", parsedURL.Scheme)
|
|
return
|
|
}
|
|
|
|
// Validate path
|
|
path := strings.Trim(parsedURL.Path, "/")
|
|
if !isValidPath(path) {
|
|
a.app.Logger.Warn("Invalid path:", path)
|
|
return
|
|
}
|
|
|
|
// Sanitize parameters
|
|
params := sanitizeQueryParams(parsedURL.Query())
|
|
|
|
// Process validated URL
|
|
a.processDeepLink(path, params)
|
|
}
|
|
|
|
func isValidPath(path string) bool {
|
|
// Only allow alphanumeric and forward slashes
|
|
validPath := regexp.MustCompile(`^[a-zA-Z0-9/]+$`)
|
|
return validPath.MatchString(path)
|
|
}
|
|
|
|
func sanitizeQueryParams(query url.Values) map[string]string {
|
|
sanitized := make(map[string]string)
|
|
for key, values := range query {
|
|
if len(values) > 0 {
|
|
// Take first value and sanitize
|
|
sanitized[key] = sanitizeString(values[0])
|
|
}
|
|
}
|
|
return sanitized
|
|
}
|
|
```
|
|
|
|
### Prevent Injection Attacks
|
|
|
|
Never execute URLs directly as code or SQL:
|
|
|
|
```go
|
|
// ❌ DON'T: Execute URL content
|
|
func badHandler(url string) {
|
|
exec.Command("sh", "-c", url).Run() // DANGEROUS!
|
|
}
|
|
|
|
// ✅ DO: Parse and validate
|
|
func goodHandler(url string) {
|
|
parsed, _ := url.Parse(url)
|
|
action := parsed.Query().Get("action")
|
|
|
|
// Whitelist allowed actions
|
|
allowed := map[string]bool{
|
|
"open": true,
|
|
"settings": true,
|
|
"help": true,
|
|
}
|
|
|
|
if allowed[action] {
|
|
handleAction(action)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Manual Testing
|
|
|
|
Test protocol handlers during development:
|
|
|
|
**Windows:**
|
|
```powershell
|
|
Start-Process "myapp://test/action?id=123"
|
|
```
|
|
|
|
**macOS:**
|
|
```bash
|
|
open "myapp://test/action?id=123"
|
|
```
|
|
|
|
**Linux:**
|
|
```bash
|
|
xdg-open "myapp://test/action?id=123"
|
|
```
|
|
|
|
### HTML Testing
|
|
|
|
Create a test HTML page:
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Protocol Test</title>
|
|
</head>
|
|
<body>
|
|
<h1>Custom Protocol Test Links</h1>
|
|
|
|
<ul>
|
|
<li><a href="myapp://open/document?id=123">Open Document 123</a></li>
|
|
<li><a href="myapp://settings/theme?mode=dark">Dark Mode Settings</a></li>
|
|
<li><a href="myapp://user/profile?username=john">User Profile</a></li>
|
|
</ul>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Protocol Not Registered
|
|
|
|
**Windows:**
|
|
- Check registry: `HKEY_CURRENT_USER\SOFTWARE\Classes\<scheme>`
|
|
- Reinstall with NSIS installer
|
|
- Verify installer ran with proper permissions
|
|
|
|
**macOS:**
|
|
- Rebuild application with `wails3 build`
|
|
- Check `Info.plist` in app bundle: `MyApp.app/Contents/Info.plist`
|
|
- Reset Launch Services: `/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill`
|
|
|
|
**Linux:**
|
|
- Check desktop file: `~/.local/share/applications/myapp.desktop`
|
|
- Update database: `update-desktop-database ~/.local/share/applications/`
|
|
- Verify handler: `xdg-mime query default x-scheme-handler/myapp`
|
|
|
|
### Application Not Launching
|
|
|
|
**Check logs:**
|
|
```go
|
|
app := application.New(application.Options{
|
|
Logger: application.NewLogger(application.LogLevelDebug),
|
|
// ...
|
|
})
|
|
```
|
|
|
|
**Common issues:**
|
|
- Application not installed in expected location
|
|
- Executable path in registration doesn't match actual location
|
|
- Permissions issues
|
|
|
|
## Best Practices
|
|
|
|
### ✅ Do
|
|
|
|
- **Use descriptive scheme names** - `mycompany-myapp` instead of `mca`
|
|
- **Validate all input** - Never trust URLs from external sources
|
|
- **Handle errors gracefully** - Log invalid URLs, don't crash
|
|
- **Provide user feedback** - Show what action was triggered
|
|
- **Test on all platforms** - Protocol handling varies
|
|
- **Document your URL structure** - Help users and integrators
|
|
|
|
### ❌ Don't
|
|
|
|
- **Don't use common scheme names** - Avoid `http`, `file`, `app`, etc.
|
|
- **Don't execute URLs as code** - Huge security risk
|
|
- **Don't expose sensitive operations** - Require confirmation for destructive actions
|
|
- **Don't assume protocols work everywhere** - Have fallback mechanisms
|
|
- **Don't forget URL encoding** - Handle special characters properly
|
|
|
|
## Next Steps
|
|
|
|
- [Windows Packaging](/guides/build/windows) - Learn about NSIS installer options
|
|
- [File Associations](/guides/distribution/file-associations) - Open files with your app
|
|
- [Single Instance](/guides/distribution/single-instance) - Prevent multiple app instances
|
|
|
|
---
|
|
|
|
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).
|