@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.
