Kindly Web View SDK, Links Reference
What happens to a link in the chat, and how the app takes over the ones it wants, on all four platforms. Companion to SKILL.md. The guide page is Link Interception
What the SDK does with a link
No link ever opens inside the chat. When the user taps a link, in a message, on a button or one that would open a new window, the SDK stops the web view's navigation and then:
Scheme | What happens |
|---|---|
http, https | Opens in the browser, or in the app that claims the link |
mailto | Opens the mail app |
tel | Opens the phone app |
sms | Opens the messages app |
Anything else, such as myapp:// or intent: | Blocked. Nothing opens and the delegate is not asked |
On iOS the SDK opens the link with UIApplication.shared.open, so the app needs no LSApplicationQueriesSchemes. On Android it starts an ACTION_VIEW intent in the BROWSABLE category, without Custom Tabs. When no app on the device can open it, nothing happens and the SDK logs a warning
Before anything opens, three rules apply, and a link that breaks one never reaches the delegate:
- Links open only while the chat is displayed
- The same link opens at most once in 1.5 seconds, so a double tap opens it once
- At most three links open in 10 seconds. A refused link does not count toward that
The delegate
The delegate is asked about every link that passed the rules above. Return true to let the SDK open it, or false to handle it in the app
iOS
import KindlyWebView
final class ChatLinks: KindlyWebViewClientDelegate {
func shouldHandleLink(url: URL) async -> Bool {
guard url.host == "myapp.example" else { return true }
await MainActor.run {
// open the matching screen in the app
}
return false
}
}
let chatLinks = ChatLinks()
KindlyWebViewSDK.delegate = chatLinks- KindlyWebViewClientDelegate has one method, shouldHandleLink(url:) async -> Bool, and a synchronous implementation satisfies it too
- The SDK holds the delegate weakly, so keep a reference to it, as chatLinks above
- The method is not isolated to the main actor, so do UI work on the main actor, as above
Android
import ai.kindly.webview.KindlyWebViewInteraction
import ai.kindly.webview.KindlyWebViewSDK
KindlyWebViewSDK.`interface` = object : KindlyWebViewInteraction {
override suspend fun shouldHandleLink(url: String): Boolean {
if (!url.startsWith("https://myapp.example/")) return true
openInApp(url)
return false
}
}- KindlyWebViewInteraction has one method, suspend fun shouldHandleLink(url: String): Boolean, which defaults to true
- The SDK calls it on the main thread, so suspend for slow work instead of blocking
- An exception counts as true
- Set interface to null to remove the delegate
Flutter
await KindlyWebViewSDK.setDelegate(shouldHandleLink: (String url) async {
if (url.startsWith('https://myapp.example/')) {
openInApp(url);
return false; // handled by the app
}
return true; // the SDK opens it
});- The callback is a KindlyWebViewLinkCallback, FutureOr<bool> Function(String url)
- An answer that throws counts as true
- clearDelegate() removes it, and leaves a delegate the app set natively alone
- The callback reaches the Flutter engine whose Dart code last called start. After a hot restart, call start and setDelegate again. When that engine goes away, links open outside the app until another engine calls start
React Native
KindlyWebViewSDK.setDelegate({
shouldHandleLink: async (url) => {
if (url.startsWith('https://myapp.example/')) {
openInApp(url);
return false; // handled by the app
}
return true; // the SDK opens it
},
});- Only an explicit false keeps the link in the app, and an answer that throws counts as true
- setDelegate(null) removes the delegate set here and leaves one the app set natively alone
- After a JavaScript reload, call setDelegate again. Until then links open outside the app
openInApp stands for the app's own navigation
On every platform the delegate stays set across start and kill
Timeouts
Who waits | Limit | No answer in time means |
|---|---|---|
The native SDK, for the delegate | 5 seconds | true: the SDK opens the link |
Flutter, for the callback | 4 seconds, KindlyWebViewSDK.linkCallbackTimeout | true |
React Native, for the callback | 4 seconds, KindlyWebViewSDK.linkCallbackTimeoutMs | true |
The wrappers answer before the native SDK gives up. On Android, React Native pauses JavaScript timers while the chat screen is in front, so the package's own 4 seconds do not fire there and the native 5 seconds apply. A delegate that answers late never holds the chat, because the navigation was already stopped
Routing a bot link into the app
A custom scheme such as myapp://orders/42 is blocked before the delegate is asked. To send the user from the chat to a screen in the app:
- Put an https link on a domain the app owns in the bot's answer or button, for example https://myapp.example/orders/42
- In the delegate, match that domain, open the screen and return false
- Return true for every other link
The chat stays open when the delegate returns false. To show the app's screen in its place, close the chat in the delegate:
KindlyWebViewSDK.closeChat()KindlyWebViewSDK.close()await KindlyWebViewSDK.closeChat();await KindlyWebViewSDK.closeChat();Debugging
Symptom | Likely cause |
|---|---|
A myapp:// link does nothing | Custom schemes are blocked. Link to https on the app's domain and intercept it |
The delegate is never asked | On iOS the delegate was released: keep a reference. In Flutter or React Native, setDelegate has not run again after a hot restart or reload |
The link opens the browser although the delegate returns false | The answer took longer than the timeout, or threw |
Nothing opens on Android | No app on the device handles the link. The log says so under the tag KindlyWebView |
A second tap does nothing | The same link opens once in 1.5 seconds, and at most three links in 10 seconds |
A link opens nothing while the chat is hidden | Links open only while the chat is displayed |
With debug logs on, the SDK logs each link's scheme and outcome, never the URL beyond its scheme and host
Security notes
- The URL comes from the chat page. Check the host and the path before acting on it, and never treat it as an instruction the app trusts
- Return false only for links the app handles. A delegate that returns false for everything silently swallows every link in the chat