@offerberries/communication-ui
v0.5.0
Published
OfferBerries Channels UI — embeddable chat surface and standalone messenger. Shares the --ob-* token layer with the ERP and Marketplace.
Readme
@offerberries/communication-ui
The Channels UI — an embeddable chat surface, and the standalone messenger at
channels.offerberriesvo.com. It talks to the Channels service
(offerberries-communication), which owns accounts, channels, messages,
documents and realtime delivery.
What this package is, and what it deliberately is not
Channels owns the conversation. The host owns the thing being discussed.
That rule is the product's, not a convention of this package — see
docs/OWNERSHIP_BOUNDARY_AND_SEQUENCE.md in the service repo. A buyer and a
seller talk in Channels, but the order never leaves the host. Channels holds
the thread and an opaque reference to the subject, and it never learns what an
order is.
Practically, that means this package will not:
- resolve your identifiers against your system,
- know what your domain objects are, or
- decide which kind of conversation is appropriate for your users.
The last one is why presets is part of the configuration rather than a
built-in table. See below.
Install
npm install @offerberries/communication-uireact, react-dom and react-router-dom are peer dependencies — you
supply them, so there is exactly one copy of each on the page. A second router
in particular is not a size problem but a correctness one: our components would
resolve against a router nothing navigates, and every link would render and do
nothing.
Configure once, before anything renders
import { configureChannels } from '@offerberries/communication-ui';
import '@offerberries/communication-ui/style.css';
configureChannels({
baseUrl: '/api',
presets: {
business: {
direct: 'acme.buyerSeller',
group: 'acme.job',
broadcast: null, // not offered here
},
},
});baseUrl
Where the Channels service is. Points both the REST client and the session store at one service.
presets — required in practice
A preset key names the template a channel is created from: who may post, who
may read, which party types are allowed. Preset keys are your domain's
words (acme.buyerSeller, acme.job), registered against your origin in the
service's Preset collection, and this package cannot invent them — choosing
between "a buyer–seller chat" and "a job chat" means knowing what a job is.
A tier with no configured presets offers no conversation types, and the UI says so explicitly rather than rendering a form that refuses every submit. If you see "No conversation types are available here", this is the field to set.
Register the preset in the service first; nothing on the client validates these
keys, and POST /channels refuses an unknown one.
Theme
Set the tier on your root element so the accent matches the host:
<html data-tier="corporate"> <!-- ERP -->
<html data-tier="business"> <!-- Marketplace -->Tier overlays are separate imports on purpose — a host picks one, and bundling both would ship a palette you do not use:
import '@offerberries/communication-ui/theme/marketplace.css';If your app authenticates its own users
This is the C-1 path, and the usual one for an embedding host: Channels never owns users. Your backend calls the service's token endpoint, gets a Channels access token for an already-authenticated person, and hands it here.
import { adoptSession } from '@offerberries/communication-ui';
const { token } = await yourBackend.mintChannelsToken();
adoptSession({
accessToken: token,
tier: 'business',
// Optional. How YOU mint a replacement, called shortly before expiry.
renew: async () => (await yourBackend.mintChannelsToken()).token,
});Everything else — account id, display name, email, tenant — is read off the token's claims. There is nothing to reassemble.
An adopted session behaves differently from one signed in through this package, in three ways that all exist to stop your users being signed out by machinery that assumes it owns them:
- It renews through your
renew, never through/auth/refresh. The service never issued this session id, so refreshing it there returns 401 — and a 401 means "revoked", which would drop the session on a timer, mid-conversation. - It survives
restore(). That call asks which sessions this device's refresh cookie covers; yours is deliberately not among them, so its absence from the answer means nothing. signOut()drops it locally and does not call/auth/revoke, because there is nothing there to revoke. It reportsrevoked: true, which is the honest answer.
It is also kept out of the account-switcher cache, since that cache means "this device can resume this account by itself" and only you can re-mint it.
Without renew, the session simply stops working when the token expires.
Nothing drops it, so you can re-adopt at any time.
The stylesheet does not reset your document
@offerberries/communication-ui/style.css carries our design tokens
(--ob-*) and our component classes (.ob-*) and nothing that reaches
outside them — no * reset, no html/body rules, no bare button,
input or a styling.
That was not always true. Until 0.5.0 the published stylesheet was the standalone app's, reset and all, so importing it restyled a host's entire document. It was found when a host's own tab bar collapsed to unstyled text the moment an embedded surface mounted — and nothing about that looked like a stylesheet problem, because the tabs still rendered, still worked and still passed their tests.
The trade is deliberate: an embedded surface inherits YOUR focus rings, scrollbars and reduced-motion policy rather than ours. A component that overrode its host's focus treatment would be a worse citizen than one that adopts it.
Embedding a surface
import { ConversationListPane, Thread, ErrorBoundary } from '@offerberries/communication-ui';
<ErrorBoundary fallback={<YourEmptyState />} onError={report}>
<Thread channelId={id} />
</ErrorBoundary>Wrap what you embed. With no boundary between us, a throw inside this
package takes down your chrome too, and you cannot see it coming. The default
fallback renders our own ErrorState, so it matches the rest of the product if
you supply nothing.
Thread takes an optional channelId; without it, it reads :channelId from
the route. The prop lets your url stay shaped around your app rather than our
routing. It does not make Thread router-free — it still uses
useNavigate, useLocation and Link internally, so render it inside a router
context either way.
Building
npm run build:lib # ESM + UMD bundles, CSS, and .d.ts into dist-lib/
npm run build:standalone # the hosted messenger
npm run typecheck
npm run lintprepublishOnly runs the typecheck and the library build, so a publish cannot
ship a stale bundle.
Licence
UNLICENSED — all rights reserved. This package is published for use by OfferBerries and its integration partners; it is not open source.
