@zynli/react-widget
v0.2.6
Published
Embed the Zynli support chat widget in React and Next.js apps
Maintainers
Readme
@zynli/react-widget
Embed the Zynli support chat widget in React and Next.js applications.
The component checks your workspace embed allowlist before loading embed.js — unauthorized host sites never see the chat bubble.
Install
npm install @zynli/react-widgetPeer dependencies: react, react-dom. For Next.js, install next (optional peer).
Quick start
import { ZynliWidget } from "@zynli/react-widget";
<ZynliWidget
tenant={process.env.NEXT_PUBLIC_ZYNLI_TENANT!}
appUrl={process.env.NEXT_PUBLIC_ZYNLI_APP_URL!}
/>In-app (logged-in) embeds
For chat inside an authenticated product (not a public marketing site):
- In your Zynli workspace: Settings → Embed → generate an identity secret, add your app origins under Allowed embed domains, and enable Require verified identity.
- Mount with
requireUser+eagerso the widget never appears anonymously and identifies immediately. - Sign users on your server (see below) via
identityUrlor theuserprop. - On SPA navigations, call
updateZynliContextso agents see the current page / plan.
import { ZynliWidget, updateZynliContext } from "@zynli/react-widget";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
export function InAppSupportChat() {
const pathname = usePathname();
useEffect(() => {
void updateZynliContext({
url: typeof window !== "undefined" ? window.location.href : undefined,
path: pathname,
});
}, [pathname]);
return (
<ZynliWidget
tenant={process.env.NEXT_PUBLIC_ZYNLI_TENANT!}
appUrl={process.env.NEXT_PUBLIC_ZYNLI_APP_URL!}
identityUrl="/api/zynli/identity"
requireUser
eager
/>
);
}requireUser— do not load until a signed user exists; teardown on logout (user: null/reset).eager— skip idle deferral in auth shells.- Allowlist preflight is fail-open (only an explicit
allowed: falseblocks mount; CORS/network errors still mount —/widgetenforces the allowlist).
Identified users
Two options — pick one:
A. identityUrl (recommended)
Point at your session endpoint that returns a signed identity. The package fetches it with cookies and calls setUser after the embed is ready.
<ZynliWidget
tenant={tenantApiKey}
appUrl="https://your-workspace.zynli.ai"
identityUrl="/api/zynli/identity"
identityCredentials="include"
/>Your endpoint should return either:
{ "user": { "id": "…", "email": "…", "name": "…", "hash": "…", "expiresAt": 1710000000, "traits": { "plan": "pro" } } }or { "authenticated": false } / { "user": null } when logged out.
B. user prop
Sign on the server and pass the object directly (or use useZynliIdentity):
import { ZynliWidget, useZynliIdentity } from "@zynli/react-widget";
function SupportChat() {
const { user } = useZynliIdentity({ url: "/api/zynli/identity" });
return (
<ZynliWidget
tenant={tenantApiKey}
appUrl={appUrl}
user={user}
/>
);
}Server signing (Node)
Payload: {tenantApiKey}|{userId}|{email or empty}|{expiresAtUnixSeconds}
HMAC-SHA256 hex, secret from Settings → Embed → Verified visitor identity.
import { createHmac } from "crypto";
export function signZynliUser(args: {
tenantApiKey: string;
secret: string;
userId: string;
email?: string;
name?: string;
traits?: Record<string, string | number | boolean>;
}) {
const expiresAt = Math.floor(Date.now() / 1000) + 86400;
const payload = [args.tenantApiKey, args.userId, args.email ?? "", expiresAt].join("|");
const hash = createHmac("sha256", args.secret).update(payload).digest("hex");
return {
id: args.userId,
email: args.email,
name: args.name,
hash,
expiresAt,
traits: args.traits,
};
}Never expose the identity secret to the browser.
Props
| Prop | Required | Description |
|------|----------|-------------|
| tenant | Yes | Workspace API key (or slug) from Settings → Embed |
| appUrl | Yes* | Workspace origin for /widget + API (data-app-url) |
| scriptUrl | No | Override embed.js URL (defaults to Spaces CDN; local appUrl → {appUrl}/embed.js) |
| offset | No | "0" (default) or "default" (16px host margin) |
| enabled | No | false to skip loading |
| locale | No | BCP-47 locale, e.g. en, sv |
| user | No | Signed identity object |
| identityUrl | No | Your API that returns { user } (cookie auth) |
| identityCredentials | No | Fetch credentials (default include) |
| identityRefreshMs | No | Re-fetch identity on an interval |
| onIdentify / onReset | No | Callbacks after identify / logout |
| requireUser | No | When true, skip loading until a signed user is available (platform embeds) |
| eager | No | Mount immediately (data-eager); use in authenticated app shells |
* Pass appUrl so the CDN loader knows which workspace hosts /widget. Override with scriptUrl only when you need a custom/self-hosted loader.
Imperative helpers
import {
setZynliUser,
resetZynliUser,
openZynliWidget,
closeZynliWidget,
toggleZynliWidget,
setZynliLocale,
updateZynliContext,
} from "@zynli/react-widget";
await setZynliUser(signedUser);
await resetZynliUser(); // logout
await updateZynliContext({ path: "/settings", plan: "pro" });
await openZynliWidget();Same surface as window.Zynli after embed.js loads.
Next.js
import { ZynliWidgetNext } from "@zynli/react-widget/next";
<ZynliWidgetNext
tenant={process.env.NEXT_PUBLIC_ZYNLI_TENANT!}
appUrl={process.env.NEXT_PUBLIC_ZYNLI_APP_URL!}
identityUrl="/api/zynli/identity"
requireUser
eager
/>Troubleshooting
| Issue | Check |
|-------|--------|
| Widget does not appear | Host origin listed under Settings → Embed → Allowed embed domains; only an explicit allowlist denial blocks the React preflight — CORS/proxy noise still mounts |
| Identity not applied | Endpoint returns hash + expiresAt; identity secret enabled; use requireUser for in-app |
| Stale user after logout | Return { user: null } from identityUrl, or user={null} / resetZynliUser() |
| “Sign in required” in widget | Workspace has Require verified identity on — call setUser / provide identityUrl |
License
Apache-2.0
