Support widget
MedianSupport props, the user object, the dashboard settings that change it, and its console messages.
import { MedianSupport } from "@mediansh/widget";
<MedianSupport apiKey="median_pk_..." />;
With an identity route, pass its path instead:
<MedianSupport identity="/api/median/identity" />;
Mount it once, near the root of your app. It places itself in the bottom right
corner. Do not wrap it in dynamic(). The reply renderer already loads on its
own when the pointer reaches the launcher, when the panel opens, or when the
page is idle with an answered thread behind the button.
What a visitor sees in a thread is on Conversations. Opening the panel from your own code is on Control from code.
Props
The props type is MedianSupportProps. launcher takes a
SupportLauncherKind, "bubble" | "hidden".
apiKey?string
The median_pk_ id from publicKeyFromMedianKey(MEDIAN_KEY). Required unless identity is set.
stringidentity?string
Path to a route built with medianIdentity. It returns the key and the signed user. Required unless apiKey is set. Pass both to show the launcher before the route answers.
stringuser?MedianUser
The signed in person. Replaces the user the identity route returns. See MedianUser below.
MedianUsertelemetry?boolean
Send time zone, language, browser, OS, device, screen and page with each message. False reads none of it.
booleantruescreenshot?boolean
Let the agent ask to see the visitor's screen. False tells the agent no picture is coming.
booleantruediagnostics?boolean | { errors?: boolean; network?: boolean; console?: boolean }
Record uncaught errors, and optionally failed requests and console.error calls, and send them with the next message.
boolean | { errors?: boolean; network?: boolean; console?: boolean }falseappState?Record<string, string | number | boolean>
Your own values for bug reports, such as a build or a route. Read when a message is sent. Sent only while diagnostics is on.
Record<string, string | number | boolean>requireSignIn?boolean
Show a Sign in to chat panel instead of the composer until user names somebody.
booleanfalseonSignIn?() => void
Runs when the visitor presses Sign in on that panel. Without it, the panel has no button.
() => voidrequireEmail?boolean
Ask for an email before the first message. Skipped when user.email is set or this browser already answered. The Ask for an email switch in the dashboard can also turn it off.
booleantruemessagePreview?boolean
Show the newest unread reply as a card over the closed launcher. The count shows either way. The Preview replies switch in the dashboard can also turn it off.
booleantruetitleCount?boolean
Put the unread count in the tab title, as (1) Your page. False never touches the title.
booleantruelauncher?"bubble" | "hidden"
Hidden renders no button. The panel still opens in the corner when your code opens it.
"bubble" | "hidden""bubble"open?boolean
Control the open state yourself. While it is set, open(), close() and toggle() from useMedianSupport and medianSupport do nothing.
booleanonOpenChange?(open: boolean) => void
Called when the launcher, the unread card, the close button, Escape, a press on the modal's backdrop, or a destination press on a phone or in the modal opens or closes the panel. Not called for your own open(), close() and toggle() calls.
(open: boolean) => voidonNavigate?(path: string) => void
Called with a path on this site when the visitor presses Take me there on a page one of your tools offered. Without it, no button is shown.
(path: string) => voidDiagnostics, screenshots, telemetry and appState limits are on
Context and diagnostics. Destinations are on
Pages and highlights.
MedianUser
Every field is optional.
| Field | Type | Effect | Limit |
|---|---|---|---|
id |
string |
Your id for this person. With a matching hash, their conversations follow them across devices |
Up to 128 characters, checked exactly as sent. Longer fails the signature check |
hash |
string |
id signed from MEDIAN_KEY on your server. Without it, id is ignored |
|
name |
string |
Shown in the queue and above the thread | Trimmed, cut at 80 characters |
email |
string |
Shown on the customer panel. Skips the email question | Trimmed, cut at 320 characters |
avatarUrl |
string |
Shown on the conversation and their messages | https only, up to 512 characters. Anything else is dropped |
metadata |
Record<string, string> |
Extra rows for your team, such as plan or seats | 16 entries. Keys and values cut at 200 characters |
const { data: session } = useSession();
<MedianSupport
apiKey="median_pk_..."
user={
session
? {
name: session.user.name,
email: session.user.email,
avatarUrl: session.user.image,
metadata: { plan: session.org.plan, seats: String(session.org.seats) },
}
: undefined
}
/>;
- Nothing is stored until the visitor sends a message. The first message saves
user, and later changes sync as they happen. Passuseron every render, as soon as your session has it. - A new
name,emailoravatarUrlreplaces the stored one. A missing field or an empty string leaves the stored value alone. metadatais replaced as a whole. Keys missing from the newest object are removed. An empty object is ignored.- Metadata keys display as labels.
signed_up_atandsignedUpAtboth show as Signed up at. Values show exactly as sent, so format dates and numbers yourself. - Invalid values are cut or dropped. They never block a message.
- Without a name or an email, your team sees the visitor as
Customer #12.
Signing id and signing out are on Identity.
Dashboard settings
These change every installed widget without a redeploy.
| Setting | Where | Effect |
|---|---|---|
| Name | Settings → General | Header title, the launcher’s label, and How can we help you with Acme? on a new conversation |
| Picture | Agent → Profile | The agent’s face in the header and on its replies |
| Name | Agent → Profile | The agent’s name on its replies |
| Ask for an email | Agent → Behavior | Off turns the email question off, whatever requireEmail says |
| Preview replies | Agent → Behavior | Off hides the unread card, whatever messagePreview says |
| Show Median branding | Settings → Billing, under Add-ons | Off hides the Powered by Median line under the composer and on the sign in panel. Needs the Pro plan. See Plans |
Powered by Median has no prop. Only the dashboard switch hides it. The widget checks every 60 seconds and shows the line again if the check fails or the paid period ends.
Troubleshooting
Errors inside the widget never break your page. The widget hides itself and prints to the console.
Errors
Printed in every build.
| Console message | Fix |
|---|---|
Median: the apiKey passed to <MedianSupport> is "…", which does not look like the browser-safe id from publicKeyFromMedianKey(MEDIAN_KEY). It should start with median_pk_. |
Pass the id from publicKeyFromMedianKey, not MEDIAN_KEY itself |
Median did not recognize the apiKey passed to <MedianSupport>, so it has been hidden. Derive it from an active MEDIAN_KEY with publicKeyFromMedianKey. |
No organization has this id. It was deleted or mistyped. Derive it from a current key in Settings → API |
Median: <MedianSupport> was given neither apiKey nor identity, so nothing is on the page. Pass the median_pk_ id from publicKeyFromMedianKey(MEDIAN_KEY), or the path of a route built with medianIdentity(). |
Pass apiKey or identity |
Median: could not read the identity route. followed by /api/median/identity answered 500. It should return { apiKey, user } from medianIdentity(). |
Fix the route. With identity alone, nothing renders. Pass apiKey too to keep the widget up |
Median: the user hash passed to <MedianSupport> did not match. Sign the same string you pass as `user.id` with the Median key that produced this widget id. Until it matches, this visitor gets a conversation of their own on every device. |
Sign the exact string you pass as user.id, with the key this widget id came from |
Median: a user hash was passed to <MedianSupport>, but its widget id and hash came from different Median keys. Derive both from the same key. |
Derive apiKey and hash from one current MEDIAN_KEY |
Median: refused to navigate to https://…, which is not a path on this site. |
A tool offered another origin. Return a path that starts with / |
Median: a tool asked to highlight <selector>, which is not a valid CSS selector. |
A tool returned a highlight the browser cannot parse. Return a valid CSS selector |
Median: mountMedianSupport() was called where there is no document, so nothing was mounted. Call it in the browser, after the page exists. |
Call it in the browser, after the page exists |
Sending from the support widget failed followed by the error |
A send failed. A red line shows above the composer and the draft stays, so the visitor can send it again |
Rating the conversation failed followed by the error |
The visitor’s rating did not save. The panel shows Could not submit your rating. Try again. |
Median: <MedianSupport> hit an error and has been hidden. Your page is unaffected. |
Any other error. The original error is printed after it |
Warnings
Printed only when your bundle sets process.env.NODE_ENV to development.
| Console message | Fix |
|---|---|
Median: more than one <MedianSupport> is mounted. They share one open state and open together. Mount it once, near the root of your app. |
Mount one MedianSupport or MedianSupportModal, not two |
Median: the widget was asked to open, but no <MedianSupport> is on the page. Mount it once, near the root of your app. |
open() ran with nothing mounted. It prints after one second, and a panel that mounts later still opens |
Median: a tool offered to take this visitor to a page, but <MedianSupport> has no onNavigate, so no button is shown for it. Pass one, such as onNavigate={router.push}. |
Pass onNavigate, such as router.push |
Median: support read status could not be synced and
Median: the picture the agent asked for could not be uploaded print in every
build and need no action.
No console message
| Symptom | Cause |
|---|---|
| The widget renders unstyled | @mediansh/widget/styles.css is not imported |
| One person shows up twice in your inbox | They used two devices without a signed user.id. See Identity |
| The sign in panel shows for a signed in person | user is empty or arrives late. Pass it as soon as your session loads |
The email question shows although you pass user |
user.email is missing or empty |