npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@keyda/bot-capacitor

v0.1.4

Published

Open your Keyda bot in an Ionic/Capacitor app: the plain widget script tag, or the hosted chat page as a full-screen surface.

Readme

@keyda/bot-capacitor

Most Ionic apps should just use the script tag. An Ionic or Capacitor app is already a web view, so the plain website widget runs inside it unchanged. No npm package, no plugin, no native build step:

<!-- src/index.html, before </body> -->
<script src="https://keyda.in/business/widget.js"
        data-key="kb_live_YOUR_CLIENT_ID" async></script>

Rebuild, run, and the launcher is there. That is the whole integration, and for most apps it is the better one: the chat floats over your own screens, your navigation keeps working behind it, and there is nothing to keep in sync.

This package exists for one case the script tag does not cover — when you want the chat presented as a separate full-screen surface over your app rather than embedded in your own DOM. It also ships a typed helper for the script tag above, because that is still the right answer for most apps.

What this actually is

Every Keyda SDK is a thin wrapper around one hosted chat page:

{baseUrl}/chat/{clientId}      baseUrl defaults to https://keyda.in/business

There is no chat UI in this package. There is no native code in it either. The full-screen surface hands that URL to the system browser view (SFSafariViewController on iOS, Custom Tabs on Android) and the embedded surface injects the same widget.js a Keyda customer pastes into a website. Both render the one hosted chat page, which is why a change an owner makes in their dashboard is live in your app immediately, with no release from you.

If "it's a web view" is a dealbreaker, it is better to know now than after you ship. CONTRACT.md explains the trade in full.

Install

npm install @keyda/bot-capacitor

Zero dependencies. For the full-screen surface, also install the optional peer:

npm install @capacitor/browser && npx cap sync

It is optional in the real sense — without it the package falls back to window.open and still works, with the limits noted under Full-screen below.

There is nothing to import: on a device, npx cap sync is enough, and this package picks the plugin up off the Capacitor bridge itself. (On the web there is no native plugin to find, so the chat opens in a new tab either way — unless your own code imports @capacitor/browser, which registers its web implementation.)

Get your client id from Install in the Keyda Business dashboard.

Option 1: embedded (recommended)

The typed equivalent of the script tag, for when you would rather configure the client id in code than in index.html:

import { embedWidget } from '@keyda/bot-capacitor';

// Once, at app bootstrap.
await embedWidget({ clientId: 'kb_live_YOUR_CLIENT_ID' });

It resolves true when the script loaded, false if it could not be fetched. It never throws for a network failure.

To open the chat from your own button instead of the floating launcher:

import { getEmbeddedWidget } from '@keyda/bot-capacitor';

getEmbeddedWidget()?.open();

Two things about the embedded widget that are easy to trip over:

  • It mounts once per page load and has no teardown. Embed it at bootstrap, not inside a component that unmounts on navigation. There is no API to remove it again, and this package does not pretend otherwise.
  • window.KeydaBot is not this package's KeydaBot. The widget publishes its own controls on that global. getEmbeddedWidget() is the typed way to reach them. The KeydaBot you import drives the full-screen surface, and the two share no state.

Option 2: full-screen

import { KeydaBot } from '@keyda/bot-capacitor';

KeydaBot.init('kb_live_YOUR_CLIENT_ID');        // once, at bootstrap

// From a button:
const opened = await KeydaBot.open();
if (!opened) {
  // Show your own "couldn't open chat, try again". This call hands off to the
  // OS and has no UI of its own to put a retry in.
}

open() also takes the config inline, so init() is optional:

await KeydaBot.open({ clientId: 'kb_live_…', baseUrl: 'https://staging.keyda.in' });

Which path it takes

| Situation | Surface | close() | isShowing | |---|---|---|---| | @capacitor/browser installed, iOS | SFSafariViewController | works | accurate | | @capacitor/browser installed, Android | Custom Tabs | see below | accurate | | No plugin, on device | system browser, via the Capacitor bridge | no | always false | | No plugin, web / ionic serve | new tab | works | accurate |

Android close(). A Custom Tab belongs to the customer's Back gesture, not to the app that launched it. close() resolves false there instead of throwing. Do not build UI that depends on dismissing the chat programmatically on Android.

Without the plugin, on device, the URL leaves your process and there is no handle back. open() resolves true because it did open, and isShowing reports false because the honest answer is that we cannot see it any more. Install @capacitor/browser if you need either of those to mean more.

On the web, open() must be called directly from the click handler. An await before it spends the user gesture and the browser blocks the popup; open() resolves false and says so in the console.

API

KeydaBot.init(clientId: string, baseUrl?: string): void
KeydaBot.open(options?: { clientId, baseUrl }): Promise<boolean>   // alias: show()
KeydaBot.close(): Promise<boolean>                                // alias: dismiss()
KeydaBot.isOpen: boolean                                          // alias: isShowing

embedWidget(options: { clientId, baseUrl? }): Promise<boolean>
getEmbeddedWidget(): EmbeddedWidget | null
chatUrl(clientId: string, baseUrl?: string): string

show/dismiss/isShowing are the names every Keyda SDK uses; open/close/isOpen are the same three operations under the names a JavaScript caller expects. They are the same functions, not variants.

That is the entire surface. There is no sendMessage, no unread count and no identify call, because there is nothing behind them yet on the server. A method that does not work end to end is worse than a missing one.

An invalid clientId throws immediately — it must match kb_live_ + 8-48 hex characters — and so does open() called before any client id has been given. Those two are the only things in this package that throw, and it is deliberate: both are mistakes in your own source that fail identically on every run, so they surface on your first launch rather than in front of a customer. Runtime failures (offline, blocked, refused) are all reported by return value.

Things this package cannot do for you

The keyboard, on the embedded path

The single most common web-view chat defect is the keyboard covering the input. On the full-screen path the system browser view handles it and there is nothing to configure. On the embedded path it is your app's window, so it is your setting:

  • iOS — leave @capacitor/keyboard's resize at its default (native). Setting resize: 'none' leaves the keyboard over the chat input.
  • Android — keep adjustResize on the activity, which is Capacitor's default. Changing it to adjustPan for some other screen affects this one.

Safe areas, on the embedded path

widget.js fills the viewport it is given and adds no inset padding of its own. On a phone screen (520px and under) the chat panel goes edge to edge. In a default Capacitor app the web view is already laid out inside the safe area, so this is fine. If you have gone edge-to-edge — viewport-fit=cover with a transparent status bar — check the panel header and the launcher on a notched device, because the widget will go edge-to-edge with you.

The full-screen path is unaffected: the system browser view draws its own chrome and handles insets itself.

Authorized domains

If the bot owner has set Authorized Domains in the dashboard, the embedded widget has to satisfy it. A Capacitor web view reports its origin as capacitor://localhost (iOS) or https://localhost (Android), and localhost always passes — so a default Capacitor app works with no dashboard change.

If you set server.hostname in capacitor.config.ts, your web view stops reporting localhost and starts reporting that hostname. Add it to Authorized Domains, or the widget loads and then silently refuses to mount.

The full-screen path is unaffected — the hosted page is served by the platform itself and is always allowed.

Theme

The chat's theme — light, dark, or matching the visitor's device — is set once by the bot owner in the dashboard, and the hosted page applies it. There is no theme option on this package and no per-app override; the owner's setting reaches every surface at once, which is the point of a single hosted renderer.

  • Embedded pathwidget.js runs on your page and is themed fully, including "Match the visitor", which follows the device's colour scheme.
  • Full-screen path — the chat inside the system browser sheet is themed by the same setting. The sheet's own chrome (the toolbar and the Done/close control) is the platform's and follows the device, not the bot: a system browser view has no bridge for the page to announce its theme through, so this package cannot make an "Always dark" bot's sheet chrome dark on a light device. The native Android, iOS, React Native and Flutter SDKs do match their chrome to the page; if that matters to you, they are the better fit.

Attachments, on the embedded path

If the bot's chat offers an attach button, the picker opens on both paths without a line of code from this package — but only one of them is your app's problem.

Full-screen path: the system browser sheet is Safari or Chrome. It owns the picker and the permission prompts, and there is nothing for you to add.

Embedded path: widget.js runs in your web view, so the picker is your app's. Capacitor's own BridgeWebChromeClient answers it on Android (asking for CAMERA at runtime only if the input requests capture and your app declares the permission), and WKWebView answers it on iOS. The iOS half needs a key in your Info.plist:

<key>NSCameraUsageDescription</key>
<string>Attach a photo to your support conversation.</string>

Add it whether or not you expect the camera to be used. WebKit's upload sheet offers Take Photo or Video for any input that accepts images — the page cannot suppress the option — and iOS terminates an app that reaches the camera with no usage description, mid-conversation, in front of the customer. Add NSMicrophoneUsageDescription as well if the chat accepts video. The photo library itself needs no key (PHPicker).

Conversation continuity

The chat keeps its conversation in DOM storage, per client id, for 24 hours. The embedded widget stores that in your app's web view; the full-screen surface stores it in the system browser's. They are separate jars, so a conversation started one way does not continue in the other. Pick one surface per app rather than offering both.

Limitations

Stated plainly, because discovering these after shipping is worse:

  • Not offline-capable. It is a hosted page. No connection, no chat.
  • No push notifications. If a customer leaves the chat, nothing brings them back.
  • No theme API. Theme and accent colour come from the dashboard and apply everywhere at once; there is no per-app override. See Theme for what the system browser sheet does and does not follow.
  • No teardown for the embedded widget. It mounts once per page load.
  • close() is unreliable on Android, and impossible without @capacitor/browser. See Which path it takes.
  • Attachments on the embedded path are your app's permissions. The picker opens with no change to this package, but an iOS app missing NSCameraUsageDescription is terminated when a customer picks "Take Photo or Video". See Attachments, on the embedded path.
  • No analytics and no device identifiers. Nothing here observes your users. That is a design constraint, not an oversight.

On outbound links: the chat now carries many of them, not just the "Powered by Keyda" footer — every URL or markdown link in a bot answer is rendered as an anchor with target="_blank" rel="noopener noreferrer". On the full-screen path those open outside the chat by construction (the system browser owns the surface). On the embedded path, what happens to a target="_blank" tap is your shell's external-navigation policy, not this package's: stock Capacitor hands it to the system browser, but a Cordova or hand-rolled WebView shell may drop it — if links in answers do nothing in your app, that is where to look.

Licence

MIT — see LICENSE at the repository root.