@yaps/support-web
v2.0.0
Published
Support Yaps web widget: <yaps-support> custom element and script-tag build.
Maintainers
Readme
@yaps/support-web
The Support Yaps web widget: a launcher bubble with an unread badge and a chat panel (a floating card on desktop, a sheet on phones), in a Shadow DOM so your styles and ours never mix. Colours, font and corners come from your Appearance or the one you publish in the inbox.
Install
Script tag (the Support server serves the built file):
<script src="https://support.yaps.ai/widget.js" data-app-id="app_123" async></script>| Attribute | |
|---|---|
| data-app-id | Your public App ID (required). |
| data-launcher | bottom-right (default), bottom-left, or hidden to use your own button. |
| data-base-url | API origin. Defaults to the origin widget.js was loaded from. |
| data-language | Force a UI language (normally the browser's). |
| data-log-level | off, error, warn (default), info, debug. |
Or from npm:
npm install @yaps/support-webimport YapsSupport from "@yaps/support-web";
YapsSupport.configure({ appId: "app_123" });Or as an element, wherever you like:
<yaps-support app-id="app_123" launcher="bottom-left"></yaps-support>
<!-- Inline, filling its container (give it a height): -->
<yaps-support app-id="app_123" mode="inline" style="height: 640px"></yaps-support>
<!-- Another server (development): -->
<yaps-support app-id="app_123" base-url="http://localhost:8787"></yaps-support>The element's app-id, base-url and launcher only apply when the SDK isn't configured yet.
Quick start
The script tag sets window.YapsSupport. Calls made before it loads can be queued:
<script>
window.YapsSupport = window.YapsSupport || [];
YapsSupport.push(["identify", { userId: "u_42", name: "Ada", email: "[email protected]" }]);
</script>
<script src="https://support.yaps.ai/widget.js" data-app-id="app_123" async></script>
<script>
window.addEventListener("yaps-support:ready", (e) => {
const Support = e.detail;
document.querySelector("#help").onclick = () => Support.open();
Support.onUnread((n) => (document.querySelector("#help-badge").textContent = n || ""));
});
</script>API
| | |
|---|---|
| configure({ appId, baseUrl?, launcher?, mount?, appearance?, languageOverride?, logLevel?, desktop? }) | Done for you by the script tag. desktop is for Tauri and Electron apps (below). |
| identify({ userId, name, email }) / identify({ token }) | Labels, or a verified identity. |
| setMetadata(record) | Replaces the whole dictionary (≤ 30 string entries). |
| logout(), deleteData() | Sign-out; deletion throws SupportError on failure. |
| open(), close(), toggle() (openChat/closeChat too) | Show or hide the panel. |
| onUnread(cb), onChatOpen(cb) | Called now and on change; return an unsubscribe function. |
| setLauncher("bottom-right" \| "bottom-left" \| "hidden") | Move or hide the bubble. |
| appearance, usesRemoteAppearance, languageOverride | Theme and language (below). |
| onEvent | chatOpened, messageSent, imageSent, fileSent, reviewOpened, paywallOpened, appPromoOpened, codeCopied, codeOpened, linkOpened, identityRejected… |
| paywalls, onPaywall, paywallAvailability, availablePaywalls, refreshPaywalls() | Offer cards. |
| screens, onScreen | Link cards that open a page or view in your app (below). |
| track(name, { opensChat }) | Reports a moment for automations. Never throws. |
| enablePush({ serviceWorkerPath }) | Web push (below). |
The panel accepts images and PDF/TXT/ZIP/LOG files from the + button, drag and drop, and paste. Images are re-encoded in the browser as JPEG ≤ 2,048 px and ≤ 1 MiB before upload.
Appearance
YapsSupport.appearance = {
accentColor: { light: "#0F766E", dark: "#2DD4BF" },
onAccentColor: "#FFFFFF",
chatBackground: { light: "#FFFFFF", dark: "#0B0B0C" },
fontFamily: "Inter",
cornerStyle: "soft",
title: { en: "Help", es: "Ayuda" },
};Remote (published in the inbox) beats local, which beats the SDK default, field by field. Changes
apply the next time the panel opens. Position and stacking are CSS custom properties on the
element: --yaps-support-offset-x, --yaps-support-offset-y, --yaps-support-z,
--yaps-support-badge.
Identity verification
Your server signs a JWT (HS256) with the app's identity secret containing user_id and exp; the
page passes it to YapsSupport.identify({ token }). Never ship the secret to the browser. Verified
customers share one conversation across their devices in this app (App ID), including its mobile
versions if they use the same App ID. Refresh the token when
onEvent reports identityRejected with identity_token_expired.
Web push
Turn on the web platform for the app in the inbox (that adds config.webPush.publicKey), serve
yaps-support-sw.js from your origin (it's in dist/, or copy it from
https://support.yaps.ai/yaps-support-sw.js), then call this from a click handler:
button.onclick = async () => {
const result = await YapsSupport.enablePush({ serviceWorkerPath: "/yaps-support-sw.js" });
// { ok: true } or { ok: false, reason: "denied" | "unsupported" | "not_enabled" | "failed" }
};The widget subscribes with PushManager and sends { provider: "webpush", token: JSON.stringify(subscription) }.
Already have a service worker? Add importScripts("/yaps-support-sw.js") to it; its handlers ignore
pushes and clicks that aren't from Support. Clicking a notification focuses your page and opens the
chat (or opens your site with #yaps-support).
Desktop apps (Tauri, Electron)
Desktop web views have no Web Push and may ignore window.open, so pass a desktop object and
your app does those jobs. The device registers as macos, windows or linux:
import YapsSupport from "@yaps/support-web";
import { openUrl } from "@tauri-apps/plugin-opener";
import { sendNotification } from "@tauri-apps/plugin-notification";
import { getCurrentWindow } from "@tauri-apps/api/window";
YapsSupport.configure({
appId: "app_123",
launcher: "hidden",
desktop: {
openUrl,
notify: ({ title, body }) => sendNotification({ title, body }),
setBadge: (count) => getCurrentWindow().setBadgeCount(count || undefined),
},
});While the chat is closed the widget checks for replies every pollIntervalSeconds and calls
notify once per new reply; checkForReplies() does it on demand. Review cards open the app's
desktop review link (a Trustpilot page, say). See the desktop guide.
Paywalls
YapsSupport.paywalls = [{ id: "yearly_offer", title: "Yearly 40% off" }];
YapsSupport.onPaywall = (id) => openCheckout(id);
YapsSupport.paywallAvailability = async (id) => isEligible(id); // optionalOffer cards are tappable only when the paywall is available and onPaywall is set. Review cards
open the store URL in a new tab.
Link cards and screens
Your team can send a link card: the message with one button, drawn like an offer card. Web pages
(https:) open in a new tab (noopener); mailto:, tel: and sms: links open in place; any
other scheme is refused. Cards that open the app's system settings are for native apps and always
read "No longer available" here. Cards can also open a page or view in your app:
YapsSupport.screens = [{ id: "edit_profile", title: "Edit profile" }]; // ≤ 50; titles are for your team
YapsSupport.onScreen = (id) => router.push(`/${id}`); // the panel has already closedWith the script tag, queue them like any call:
YapsSupport.push(["screens", [{ id: "edit_profile", title: "Edit profile" }]]). A screen card is
tappable only when its screen is in screens and onScreen is set; otherwise it reads "No longer
available". Clicking it closes the panel (not an inline <yaps-support mode="inline"> chat), then
calls onScreen. Screens are reported to the inbox like paywalls.
| Event | Fields |
|---|---|
| linkOpened | messageId, target (url or screen), screenId?, linkId? |
linkOpened and the open receipt follow the open (the new tab was requested, or onScreen was
called); if onScreen throws, neither is sent. http/https links in message text are clickable and
open in a new tab; email addresses stay plain text.
App promo and code cards
Your team can promote another of your apps or send a discount code. Cards arrive resolved for the web, so there's nothing to set up.
- App promo: the app's icon, name and pitch with Get it, which opens the web store listing
in a new tab (
noopener). If the customer already uses the app it says "You already have it" with no button; with no listing for the web only the message text shows. - Code: the code in a monospaced box with Copy and Redeem. Copy uses
navigator.clipboard.writeText(secure contexts; iframes needallow="clipboard-write"); when that's refused it selects the code and says "Select the code to copy it". Redeem opens the campaign's link in a new tab (noopener). A code for iPhone or Android shows Copy and "Redeem on iPhone" / "Redeem on Android" without Redeem. Past its expiry date the code is struck through with "Expired" and no buttons, and a code on screen turns expired when the time comes.
| Event | Fields |
|---|---|
| appPromoOpened | messageId, url, promotedAppId |
| codeCopied | messageId, campaignId |
| codeOpened | messageId, campaignId |
Each click also sends the action receipt (open for Get it and Redeem, copy for Copy). The
buttons are real buttons with the widget's focus ring, so Tab, Enter and Space work; screen readers
hear the code spelled out ("Code: S T U D E N T 2 0") and an announcement when it's copied. The
code stays left to right in right-to-left chats.
Development
npm run build -w @yaps/support-web # dist/widget.js (IIFE), dist/index.js (ESM), dist/yaps-support-sw.js
npm test -w @yaps/support-web # vitest + happy-domdemo-mock.html: the widget against a stubbed server (demo/mock.js), on a page with deliberately hostile styles. Serve the folder (npx vite sdk/web) and open/demo-mock.html?open=1(?cards=states&open=1adds the installed, other-platform and expired card states, and link cards).demo.html: the widget against the Worker onhttp://localhost:8787(/demo.html?appId=app_…).
widget.js is about 52 KB gzipped; the build fails above 60 KB.
