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

@webority/support-react

v0.125.0

Published

Webority customer support for React portals: ticket centre, report-an-issue with an optional page screenshot, and support contact details, built on @webority/ui-react.

Readme

@webority/support-react

Customer support for a React portal: a ticket centre, live chat with an AI assistant and a support agent, a report-an-issue button that can attach a screenshot of the page when the person asks for one, and support contact details. It talks to the Webority Support API that the product's backend hosts, and it is built on @webority/ui-react and @webority/theme, so it looks like the rest of the portal with no extra CSS.

Install

npm install @webority/support-react @webority/chat-react @webority/ui-react react-bootstrap [email protected]

@webority/ui-react, react, react-dom, react-bootstrap and bootstrap are peer dependencies: the portal already has them. Pin @webority/support-react to the same exact version as @webority/ui-react. @webority/chat-react is a peer too (the chat screen reuses its composer and Markdown): pin it to the same exact version. The screenshot uses html-to-image, which installs with the package.

Set up the provider

Give the provider a client built on the portal's own HTTP instance. That instance already carries the bearer token and refreshes it, so the support calls sign in the same way as every other call. Attachments are fetched through it too, so private files never need a public URL.

import { createSupportClient, SupportProvider } from "@webority/support-react";
import { userClient } from "./services/api/clients";

const supportClient = createSupportClient(userClient, "/api/support");

export function App() {
    return (
        <SupportProvider client={supportClient}>
            <Routes />
        </SupportProvider>
    );
}

SupportProvider also takes timeZone, the IANA zone every ticket and message time is shown in. It defaults to Asia/Kolkata; times read 06-Oct-2026, 1:10 PM.

SupportProvider also takes renderTime, a function that draws every ticket and message time from its ISO string instead of the built-in date and time. When it is set, the "Times are shown in" note is left out, because the product's own rendering decides the zone.

<SupportProvider client={client} renderTime={(iso) => <ScreenTime value={iso} />}>
</SupportProvider>

createSupportClient(http, basePath = "/api/support") accepts any axios-like instance with get(url, config) and post(url, data, config) that resolve to { data }. The second argument is the base path as a string, or as { base } the way createChatClient(http, { base }) takes it: createSupportClient(userClient, { base: "/api/support" }). Every method rejects with a SupportApiError carrying code, status and a message written for the person, which the components show as-is.

The chat takes two objects built once, over the same signed-in instance:

const chatClient = createSupportChatClient({ http: userClient, baseUrl: "/api/support" });
const connection = createSseChatConnection({
    http: userClient,
    baseUrl: "/api/support",
    retryDelaysMs: [1000, 2000, 5000, 10000],
});

A host on another origin must allow the Idempotency-Key request header in its CORS policy, or retried messages fail.

createSupportChatClient({ http, baseUrl = "/api/support" }) sends the chat's requests. createSseChatConnection({ http, baseUrl = "/api/support", retryDelaysMs, stallTimeoutMs = 45000 }) opens the live stream. retryDelaysMs is the wait in milliseconds before each reconnect; the last entry repeats. stallTimeoutMs is how long the stream may go quiet before it is reopened.

listTickets returns one page, most recently updated first, in the shape { items, page, pageSize, totalCount, totalPages }. Every query part is optional: status is one of Open, InProgress, Resolved or Closed, search matches the subject or the ticket number, page starts at 1, and the API caps pageSize at 100. getTicketCounts returns { open, inProgress, resolved, closed, total }. A ticket's accountId and realm are null on the customer side, and its version is for support staff only; the components use none of them.

| Method | Request | |---|---| | getConfig() | GET {base}/settings | | listTickets({ status, search, page, pageSize }) | GET {base}/tickets?status=&search=&page=&pageSize=, one page | | getTicketCounts() | GET {base}/tickets/counts | | getTicket(id) | GET {base}/tickets/{id} | | createTicket({ category, subject, body, pageUrl, userAgent, appVersion, files, screenshots }) | POST {base}/tickets, multipart | | reply(id, { body, files, screenshots }) | POST {base}/tickets/{id}/replies, multipart | | reopen(id) | POST {base}/tickets/{id}/reopen | | rate(id, { satisfaction, comment }) | POST {base}/tickets/{id}/rating, JSON | | fetchAttachment(id) | GET {base}/attachments/{id}, returns a Blob | | confirmAction(token) | POST {base}/actions/confirm, JSON { token }, returns the ticket |

confirmAction is the person's yes to a support action an assistant prepared: raise a ticket, reply on one, or reopen one. Only this call completes the action. It resolves to the ticket it acted on (with id and number), and an expired or unknown token rejects with the code action_expired. @webority/chat-react's ActionCard takes it as its confirmAction.

Components

<SupportCenter />

The support page: the customer's tickets with status tabs and a search box, one ticket's conversation, and a New ticket dialog. The server filters, searches and pages the list, so it stays quick however many tickets a customer has. The tabs are All, Open, In progress, Resolved and Closed, each labelled with its count from getTicketCounts. All is chosen first. The search asks the server once typing pauses, and a new tab or search starts again at page 1.

<SupportCenter appVersion={import.meta.env.VITE_APP_VERSION} defaultCategory="Question" />

| Prop | What it does | |---|---| | initialTicketId | Open this ticket first. | | defaultCategory | Preselected in the New ticket form when the config lists it. | | appVersion | Recorded on new tickets so support knows which build you were on. |

Support emails link to the support page with ?ticket=<id>. The centre opens that ticket and keeps the parameter in step as tickets open and close, so a refresh stays on the same ticket. In a ticket the customer can reply with files (pasting an image into the message attaches it), reopen a settled ticket, and say whether it solved their problem. The ticket's Ticket details card beside the thread lists its status, category, number and times. Each attachment is a chip that downloads when pressed and opens in a new tab, so a ticket fetches no file until one is asked for. Files are checked against the config's type, size and count limits before anything uploads, and a file that breaks a limit gets one message, in the package's words. The New ticket form shows suggested answers as plain text under the subject once it has three characters and the person pauses; a 404 from GET {base}/suggestions means none.

<ReportIssue />

A button that opens a dialog at once to describe the problem. The person can add images by picking, dropping or pasting them. The ticket records the page address, the browser and the product version.

<ReportIssue floating appVersion={version} getTicketHref={(id) => `/support?ticket=${id}`} />

| Prop | What it does | |---|---| | label | Button text. Default "Report an issue". | | floating | Pin the button to the bottom corner of the screen. | | compact | Icon only: a 40px circle, the label as its tooltip and accessible name. With floating it sits in the shared floating stack. | | defaultCategory | Preselected when the config lists it. Default "Bug report". | | appVersion | Recorded on the ticket. | | getTicketHref(id) | Shows a View ticket link to the new ticket. | | onOpenTicket(ticket) | Called by the View ticket button when there is no href. | | onCreated(ticket) | Called once the ticket is raised. | | className | Added to the button's wrapper, floating or inline, to place it. | | style | Merged over the wrapper's own style. |

Place the button with className or style. Both land on the wrapper that holds the button, which is the element that floats. With floating, an inset moves it clear of other fixed controls such as a chat launcher:

<ReportIssue floating style={{ bottom: "5rem" }} />
<ReportIssue className="ms-auto" />

With compact and floating, the button joins the shared floating stack (useFloatingStack from @webority/ui-react): one fixed container in the bottom-end corner that the compact chat launcher from @webority/chat-react also uses. The two stack with a gap, the chat launcher in the corner and this button above it, so neither needs an offset. className and style then land on the item inside the stack, which is not itself fixed.

<ChatLauncher compact onClick={openChat} />
<ReportIssue floating compact appVersion={version} />

Nothing on the page is captured unless the person asks. The dialog has an "Add a screenshot" button, with the line "Only what is on your screen now. You can remove it before sending." under it. Pressing it hides the dialog and its backdrop, shows a small "Taking a screenshot" status, takes a picture of the page, and brings the dialog back with the picture as a thumbnail. What they typed is kept, and focus returns to the screenshot buttons. They can then retake it or remove it. If the capture fails, the dialog says so and the report can still be sent without one. There is no prop to capture on open.

The report button, the dialog and the status are left out of the picture. Anything else the page marks with data-support-capture="exclude" is left out too.

<SupportContact />

The support email with a copy button, the phone number as a call link, and the hours, from the support config. A field the product has not set is left out, and nothing renders when none is.

<SupportContact />

<SupportChat />

A live chat drawer. The AI assistant answers first. "Talk to a person" brings a support agent into the same conversation, and the whole chat is kept on the customer's support ticket.

<SupportProvider client={supportClient}>
    <SupportChatLauncher onClick={() => setOpen(true)} open={open} />
    <SupportChat
        chatClient={chatClient}
        connection={connection}
        open={open}
        onClose={() => setOpen(false)}
        onOpenTicket={(ticket) => navigate(`/support?ticket=${ticket.id}`)}
    />
</SupportProvider>

Build the two clients once, outside render, as shown under set up, and keep the component inside SupportProvider: the chat reads the support config for the message length and the time zone.

| Prop | Default | What it does | |---|---|---| | chatClient | required | The client from createSupportChatClient. | | connection | required | The live stream from createSseChatConnection. | | open | required | Shows the drawer. The session runs only while it is open. | | onClose | required | Called when the drawer closes. | | appVersion | none | Sent when the chat starts, so support knows which build the customer was on. | | start | none | A hand-over into the chat: { chatConversationReference, openingLine, wantsPerson }. A new object starts the chat again, so keep it in state or useMemo. | | onOpenTicket({ id, number }) | none | Shows a View ticket button when given. | | resolveLink(href) | none | Decides where a link in a message goes; passed to the Markdown renderer. | | linkComponent | none | The component that draws links in messages; passed to the Markdown renderer. | | title | "Chat with us" | The drawer's heading. | | width | "440px" | The drawer's width. |

The chat moves through these states:

  • Assistant answering: the assistant's reply appears as it is written.
  • Waiting: "Waiting for a person", with the number of people ahead in the queue.
  • With an agent: the agent's name shows in the status.
  • Ended: the reason the chat closed, with the ticket number when one was raised.

Notes: closing the drawer does not end the chat; reopening it returns the same open chat with every line. A message that fails shows Try again and keeps its key, so a retry never posts twice. The chat needs the /chats routes of the support API. The live stream is a long-lived GET that needs the browser's XHR adapter (the axios default); with the fetch adapter the events arrive all at once when the request ends.

<SupportChatLauncher />

The button that opens the chat. It sits in the bottom corner of the page.

<SupportChatLauncher onClick={() => setOpen(true)} open={open} />
<SupportChatLauncher onClick={() => setOpen(true)} compact position="start" />

| Prop | Default | What it does | |---|---|---| | onClick | required | Called when the button is pressed. | | open | false | Sets aria-expanded, for a launcher whose drawer is open. | | label | "Chat with us" | The button text; with compact it is the tooltip and accessible name. | | position | "end" | The bottom corner it sits in: "end" or "start". | | icon | "message-circle" | The icon's name. | | compact | false | Icon only, a circle in the shared floating stack. | | className | none | Added to the button's wrapper, to place it. | | style | none | Merged over the wrapper's own style. |

Place it with className or style, as with <ReportIssue />. With compact, it joins the shared floating stack beside <ReportIssue /> and needs no offset.

Hooks

useSupportConfig(), useSupportTickets(query), useSupportTicketCounts() and useSupportTicket(id) return the data with loading, error and reload. useSupportTickets takes the same query as listTickets and returns tickets (the page's items), totalCount and totalPages; it asks again whenever the query changes. useSupportTicketCounts returns counts. useSupportTicket also returns reply, reopen and rate, each resolving to the updated ticket. useSupportClient() returns the client itself.

Display rules

Status text always comes from the API's statusLabel. Times show in the viewer's own time zone with the zone named.