Median

Support widget

MedianSupport props, the user object, the dashboard settings that change it, and its console messages.

Updated Oct 1, 20267 minute read
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".

NameTypeDescription
apiKeystringThe median_pk_ id from publicKeyFromMedianKey(MEDIAN_KEY). Required unless identity is set.
identitystringPath 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.
userMedianUserThe signed in person. Replaces the user the identity route returns. See MedianUser below.
telemetrybooleanSend time zone, language, browser, OS, device, screen and page with each message. False reads none of it. Default: true.
screenshotbooleanLet the agent ask to see the visitor's screen. False tells the agent no picture is coming. Default: true.
diagnosticsboolean | { errors?: boolean; network?: boolean; console?: boolean }Record uncaught errors, and optionally failed requests and console.error calls, and send them with the next message. Default: false.
appStateRecord<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.
requireSignInbooleanShow a Sign in to chat panel instead of the composer until user names somebody. Default: false.
onSignIn() => voidRuns when the visitor presses Sign in on that panel. Without it, the panel has no button.
requireEmailbooleanAsk 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. Default: true.
messagePreviewbooleanShow 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. Default: true.
titleCountbooleanPut the unread count in the tab title, as (1) Your page. False never touches the title. Default: true.
launcher"bubble" | "hidden"Hidden renders no button. The panel still opens in the corner when your code opens it. Default: "bubble".
openbooleanControl the open state yourself. While it is set, open(), close() and toggle() from useMedianSupport and medianSupport do nothing.
onOpenChange(open: boolean) => voidCalled 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.
onNavigate(path: string) => voidCalled 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.

Diagnostics, screenshots, telemetry and appState limits are on Context and diagnostics. Destinations are on Pages and highlights.

MedianUser

Every field is optional.

FieldTypeEffectLimit
idstringYour id for this person. With a matching hash, their conversations follow them across devicesUp to 128 characters, checked exactly as sent. Longer fails the signature check
hashstringid signed from MEDIAN_KEY on your server. Without it, id is ignored
namestringShown in the queue and above the threadTrimmed, cut at 80 characters
emailstringShown on the customer panel. Skips the email questionTrimmed, cut at 320 characters
avatarUrlstringShown on the conversation and their messageshttps only, up to 512 characters. Anything else is dropped
metadataRecord<string, string>Extra rows for your team, such as plan or seats16 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. Pass user on every render, as soon as your session has it.
  • A new name, email or avatarUrl replaces the stored one. A missing field or an empty string leaves the stored value alone.
  • metadata is replaced as a whole. Keys missing from the newest object are removed. An empty object is ignored.
  • Metadata keys display as labels. signed_up_at and signedUpAt both 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.

SettingWhereEffect
NameSettings → GeneralHeader title, the launcher's label, and How can we help you with Acme? on a new conversation
PictureAgent → ProfileThe agent's face in the header and on its replies
NameAgent → ProfileThe agent's name on its replies
Ask for an emailAgent → BehaviorOff turns the email question off, whatever requireEmail says
Preview repliesAgent → BehaviorOff hides the unread card, whatever messagePreview says
Show Median brandingSettings → Billing, under Add-onsOff 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 messageFix
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 errorA 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 errorThe 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 messageFix
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

SymptomCause
The widget renders unstyled@mediansh/widget/styles.css is not imported
One person shows up twice in your inboxThey used two devices without a signed user.id. See Identity
The sign in panel shows for a signed in personuser is empty or arrives late. Pass it as soon as your session loads
The email question shows although you pass useruser.email is missing or empty

Still need help?

    Esc