<goma-messenger>
Social messenger dock. Renders a floating bubble that opens a full direct-message and group-chat panel (conversation lobby, friends and friend requests, group management, message reactions, typing indicators, read receipts, and an optional "share a bet" screen) backed by a GomaChat service over REST plus Socket.IO. Its theme tokens are the shared --goma-* set that every other widget uses; its events keep the gc:* namespace it shares with the other chat widget rather than the sports family's goma:*.
Its sister widget goma-live-chat covers the other chat surface: a public room scoped to a single match. The two share a backend and, in a correctly wired host, a single chat identity.
Element tag
html
<goma-messenger></goma-messenger>Installation
bash
pnpm add @gomagaming/messenger vue pinia vue-router vue-i18n @vueuse/coreThe five framework packages are peerDependencies, not bundled: install them once at the application level and every @gomagaming/* widget on the page shares one copy. Three of them (vue, pinia, vue-i18n) are what this widget's own code externalises. The other two are inherited: @gomagaming/my-bets is a declared dependency of this package, so your package manager installs it for you, and it in turn pulls in @gomagaming/core and @gomagaming/sports-domain, which declare vue-router and @vueuse/core as peers. Add autobahn-browser as well if you hand the embedded <goma-my-bets> a config.socketUrl for live open-bet scores: see <goma-my-bets>.
@gomagaming/messenger is a private scoped package, so the one-time .npmrc token setup in Installation applies here too: without it the install 404s.
The package is ESM-only. The exports map declares an import condition and nothing else, so there is no require() entry and no UMD/IIFE build for a <script src> tag. No TypeScript declarations ship: there is no .d.ts anywhere in the tarball. What the package does ship for tooling is custom-elements.json, a Custom Elements Manifest describing every attribute, property, method and event.
The share screen is a lazy chunk, not part of first load. dist/messenger.js is a thin entry; the bulk sits in dist/chunks/, and @gomagaming/my-bets is reached only from dist/chunks/ShareTickets-*.js, pulled in by a defineAsyncComponent(() => import('./ShareTickets.vue')) behind a v-if on ticket sharing. The build preset sets inlineDynamicImports: false, so that split survives into the published output: grep the shipped dist/messenger.js and the main chunk and you will find no reference to my-bets beyond a JSDoc comment. Turn ticket sharing off, or leave the share screen unopened, and the whole my-bets → sports-domain chain never evaluates. It still has to resolve, which is why the peer set above is not optional.
Bundle
- Entry plus chunks, gzipped
- 97 kB (note)
- ≈404 kB raw; budget is 105 kB gzip
- ShareTickets chunk, gzipped
- 2.7 kB (good)
- Only fetched when the share screen opens
Import once at your application entry: the module registers the element as a side effect:
js
import '@gomagaming/messenger'There is no exported define() function. Registration is guarded by a customElements.get() check, so importing more than once is safe.
There is no stylesheet to import, and nothing to import it from. All widget CSS lives in the package's src/styles/main.css, is inlined into the JS at build time and injected into the shadow root via adoptedStyleSheets. The build emits no dist/style.css (with no <style scoped> block left in any component, Vue has no per-component CSS to extract) and the published tarball carries only dist/messenger.js, dist/chunks/, dist/playground.js, custom-elements.json, the README and the CHANGELOG. clientstyling / clientstylingurl are the supported channels for extra CSS: see Theming.
Configuration
Every configuration key can be set two ways: as its own HTML attribute, or as a key on the config object property. Both write into the same reactive state, so they compose rather than compete: assigning config is a shallow merge that touches only the keys present in the object you hand it, and for any given key whichever path ran last wins.
js
el.setAttribute('api-url', '/goma-chat')
el.config = { locale: 'pt' } // does not clear apiUrl
el.config = { authToken: token } // merges again
el.config.locale // reading returns the live resolved statemyBets, getTickets and getMockTips are JS-property-only: an object or a callback cannot round-trip through a string attribute.
These runtime limits are compiled in and not configurable:
Runtime limits
- Characters per message
- 150 (caution)
- REST request timeout
- 10s (caution)
- Reconnect attempts
- 10 (caution)
- 1s backoff rising to 30s
- Typing indicator expiry
- 2s
- Client-side; there is no stop event
- Lobby page cap
- 50 (note)
- Conversation pages followed before it warns
- Bundled locales
- 5 (note)
- en, pt, fr, tr, da
A working minimum is four attributes:
html
<goma-messenger
api-url="/goma-chat"
socket-url="/"
auth-token="<chat access token>"
user-chat-id="<chat user id>"
></goma-messenger>Both URLs point at the chat service. In the reference deployment the host proxies it same-origin, so api-url is the relative /goma-chat and socket-url is /: Socket.IO parses a URL's path component as a namespace, so it cannot be given the REST prefix and must be proxied separately at /socket.io/. Every example on this page uses that form; see Local development and Deployment notes for the proxy on each side.
Validation is narrower than it looks. A gc:error with code MISSING_ATTRIBUTE and an attributes array is emitted only when api-url is present and socket-url is missing, because the check lives inside the bootstrap that a missing api-url prevents from running at all. The four attributes fail differently. Only a missing auth-token renders nothing at all. A missing api-url still renders the bubble: the render gate is the token alone, but never bootstraps, so nothing is dispatched. A missing socket-auth-token or user-chat-id is not validated at all: the widget renders, bootstraps and connects, and breaks only later (an absent user-chat-id stops it recognising your own messages).
The bootstrap watcher observes authToken alone, so deliver the rest of the connection state with it or before it:
Attribute order
- Connection attributesapi-url, socket-url, api-key
- Identity attributesuser-chat-id, username, user-code, socket-auth-token
- auth-token, last (good)This is what starts the connection.
An api-url that arrives after the token does not start anything. The widget connects only when both authToken and apiUrl are truthy at the moment the check runs, and only a change to authToken re-runs it. So a token set in one task followed by an api-url set in a later one leaves the widget idle until the token changes again. Handing over every key in a single config assignment is the simplest way not to have to think about it:
js
el.config = {
apiUrl: '/goma-chat',
socketUrl: '/',
userChatId: chatUserId,
socketAuthToken: chatToken,
authToken: chatToken,
}Authentication
The widget takes token strings and validates none of them. There is no login provider, no async callback and no sessionId property. A SessionProvider shape exists internally, but the web implementation is a thin closure over the config values.
REST calls carry Authorization: Bearer <auth-token>, plus x-api-key: <api-key> when that attribute is set. The socket handshake sends both tokens, in both the query string and the auth payload:
js
io(socketUrl, {
query: { EIO: '4', authToken },
auth: { token: socketAuthToken, authToken },
transports: ['polling', 'websocket'],
path: '/socket.io/',
})In practice both attributes receive the same JWT.
Getting a token
The widget accepts a token; it does not tell you how to mint one. The chat service exposes one endpoint for that (POST /auth/exchange) which trades a platform session for a chat identity:
js
const res = await fetch('/goma-chat/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: 'everymatrix', sessionId, username }),
})
const { accessToken, refreshToken, user } = await res.json()
el.config = {
socketAuthToken: accessToken,
userChatId: user.id,
username: user.username,
userCode: user.friendCode,
authToken: accessToken,
}No secret is involved: the chat service verifies sessionId against the platform itself, server-side, so this call is safe from a browser. username is a display seed only: it names a brand-new chat user and is ignored for an existing one, so the name you get back in user.username may not be the one you sent.
Read Chat identity and the provider model before integrating. It covers the two exchange paths and when each needs a shared key, the full request/response and error contract, the EveryMatrix provider end to end with its configuration, and what changes if you run a different platform or your own backend.
Run the exchange once per player, not once per widget. If you mount both <goma-messenger> and <goma-live-chat>, exchange once and feed both widgets the same accessToken and user id. The exchange itself is idempotent (the chat user is keyed on the platform's stable player id, so a second call resolves to the same chat identity rather than creating another) but it is a wasted round-trip against a rate-limited endpoint, and the two widgets holding different tokens for one player is a needless source of confusion.
Logging out
removeAttribute('auth-token') does not log the user out
The attribute handler ignores removals for every attribute except theme and clientstyling, so the old token stays in the widget's config and the teardown branch never runs. The socket stays connected and the panel keeps its data.
Set the value to an empty string instead:
js
el.setAttribute('auth-token', '') // works
el.config = { authToken: '' } // works
el.removeAttribute('auth-token') // silently does nothingOlder integration notes describe removeAttribute as the logout path. It is not: use one of the empty-string forms above.
An empty token disconnects the socket, resets the store, closes the panel, and returns the navigation to the lobby.
Expiry and re-auth
There is no session-expiry event and no 401/403 handling anywhere. An expired token surfaces only as a generic failure:
| Failure | Signal |
|---|---|
| REST rejects a call | gc:error { code: 'API_LOAD_FAILED' }: the 401 is flattened into the message string |
| Socket handshake rejected | gc:error { code: 'SOCKET_CONNECT_ERROR' } |
| 10 reconnects exhausted | gc:disconnected { reason: 'reconnect_failed' }, then gc:error { code: 'SOCKET_RECONNECT_FAILED' } |
Treat SOCKET_RECONNECT_FAILED as the session-death contract: get a new access token (POST /auth/refresh with the refresh token from the exchange avoids a full re-exchange) and write it back.
A changing auth-token forces a full reconnect. Setting the attribute to a different value disconnects the socket, reconnects, and reloads the lobby. Writing the same value again through the attribute is a no-op (the element short-circuits on an unchanged attribute) so a re-render that re-applies an identical token costs nothing. It is a changing token, not a repeated write, that forces the reconnect.
Data sources
All data comes from one GomaChat service exposing the Goma Social API v1 and a Socket.IO server speaking protocol revision 5 over Engine.IO 4 (what socket.io 4.x serves, not what "Socket.IO protocol v4" means). Point api-url and socket-url at it; in the reference deployment they are the same origin.
REST
Base URL is api-url. Every call carries the auth headers above and aborts after 10 seconds.
| Method | Path |
|---|---|
GET | /social/v1/chatrooms[?cursor=] |
POST | /social/v1/chatrooms { name, friend_user_ids } |
PUT | /social/v1/chatrooms/:id { name } |
DELETE | /social/v1/chatrooms/:id |
POST | /social/v1/chatrooms/:id/leave |
POST | /social/v1/chatrooms/:id/members { friend_user_ids } |
DELETE | /social/v1/chatrooms/:id/members/:userId |
GET | /social/v1/chatrooms/:id/messages[?cursor=] |
GET | /social/v1/friends |
GET | /social/v1/friends/pending |
POST | /social/v1/friends { friend_code } |
POST | /social/v1/friends/by-user { user_id } |
POST | /social/v1/friends/pending/:id/approve |
DELETE | /social/v1/friends/pending/:id/reject |
DELETE | /social/v1/friends/:id |
GET | /users/v1/code/:code |
GET | /social/v1/notifications |
PUT | /social/v1/notifications/:id/read |
POST | /social/v1/blocks { blocked_user_id } |
DELETE | /social/v1/blocks/:blocked_user_id |
The lobby is loaded in full. /social/v1/chatrooms is paginated and the reference server caps a page at 20, so a single call would truncate the conversation list for anyone with more than that: silently, because the lobby has no paging affordance and renders whatever it is handed. The widget follows next_cursor to exhaustion instead, bounded at 50 pages; a server that keeps handing back a cursor past that bound stops the walk and logs a console.warn rather than spinning forever. If you are implementing this endpoint, a stable cursor and a terminating next_cursor are the contract.
Socket.IO
Path is hard-coded to /socket.io/ and is not configurable. Transports are ['polling', 'websocket'], with 10 reconnect attempts on a 1 s → 30 s backoff.
Client → server:
| Event | Payload |
|---|---|
social.chatrooms.join | { id } |
social.chatrooms.message | { id, message, repliedMessage, attachment, toAi } |
social.chatrooms.messages.read | { id, message_id } |
social.chatrooms.message.react | { conversationId, messageId, emoji } |
typing_start | { conversation_id } |
Server → client:
| Event | Payload |
|---|---|
social.chatroom.{id} | { users } on join, { messages } for traffic |
presence_updated | { user_id, is_online } |
typing_indicator | { conversation_id, typing_users } |
reaction_updated | { chatroom_id, message_id, reactions } |
read_receipt_updated | { conversation_id, message_id, user_id, status } |
message_delivered | { conversation_id, message_id, status } |
conversation_read | { conversation_id, last_read_message_id } |
notification | friend requests, acceptances: also triggers a refetch |
conversation_created | { conversation_id } |
friend_removed | { user_id } |
group_updated | { conversation_id, update_type, new_name } |
group_member_added | { conversation_id, member } |
member_removed | { conversation_id, user_id, reason } |
There is no "typing stopped" event. The server sends typing_indicator only. The client expires each indicator locally after 2 seconds, so a backend implementing this protocol does not need a stop signal.
Demo mode
demo bypasses the socket and the API, but only when no auth-token is set. Supplying both starts a real connection first and then replaces the manager with the demo one, orphaning the live socket for the element's lifetime, so use demo on its own. It is not an inert fixture either: it starts scripted intervals (roughly every 9 s for groups, 22 s for direct messages, plus a 90 s tip and a 40 s notification) and accepts writes locally that never reach a server. Assign getMockTips to supply fixture content.
Props / attributes
Every row in the first table is reachable both as the HTML attribute shown and as the config key of the same name in camelCase: api-url and config.apiUrl write the same state. There are no individual per-key accessors (el.apiUrl is not a property); the object properties in the second table are the only JS-side surface.
| Attribute | Config key | Default | Description |
|---|---|---|---|
api-url | apiUrl | '' | REST base URL. Required and validated. |
socket-url | socketUrl | '' | Socket.IO server URL. Required and validated. |
auth-token | authToken | '' | Bearer token. Gates rendering and drives bootstrap. |
socket-auth-token | socketAuthToken | undefined | Token sent as the socket auth.token. Not validated. |
user-chat-id | userChatId | '' | Identifies the viewer's own messages. Not validated. |
username | username | - | Sender name on optimistic sends. |
user-code | userCode | - | The viewer's friend code, shown and copyable in Add Friends. |
user-avatar | userAvatar | - | Avatar URL. |
locale | locale | 'en' | One of en, pt, fr, tr, da. |
position | position | 'fixed' | 'fixed' (bubble plus panel) or 'inline'. Any other value is treated as fixed. |
bubble-variant | bubbleVariant | 'primary' | Floating bubble style. 'primary' = the 56px primary floater (--goma-highlightPrimary fill, dark icon). 'secondary' = the 48px secondary floater (--goma-secondaryFloater fill, falling back to --goma-highlightSecondary, white icon), for hosts whose primary floater is already taken, e.g. a mobile betslip button. Any other value is treated as 'primary'. Only applies in position='fixed'. |
enable-ticket-sharing | enableTicketSharing | false | Enables the share-a-bet screen. The button stays disabled until you also assign the getTickets property: set the flag alone and users see a permanently greyed-out control. |
theme | theme | {} | JSON string of --goma-* overrides. See Theming. |
icon-base-url | iconBaseUrl | - | Base URL for icon assets. |
api-key | apiKey | - | Sent as x-api-key. |
font-url | fontUrl | Exo 2 on Google Fonts | Set to 'none' to disable font injection. |
demo | demo | - | Bypasses the socket and API. |
clientstyling | - | - | Raw CSS appended to the shadow root. Attribute only. |
clientstylingurl | - | - | URL fetched for the same purpose. Attribute only. |
translationurl | - | - | URL of a { locale: messages } JSON bundle. Attribute only. |
| Property | Default | Description |
|---|---|---|
config | resolved defaults | Getter/setter. Reading returns the live resolved config object; assigning shallow-merges the keys you supply and applies theme if present. See Configuration. |
theme | {} | Getter/setter. Assigning routes through applyTheme(). Reading returns the --goma-* properties the widget actually painted, keyed by their normalised form: deliberately not a mirror of config.theme, which echoes back whatever raw object was last assigned including keys that were dropped. |
myBets | undefined | { config?, sessionId?, theme? }, forwarded verbatim to the embedded <goma-my-bets>. |
getTickets | undefined | See the note under Bet sharing. |
getMockTips | undefined | Demo-mode fixture supplier. |
app-url (appUrl) is accepted, mapped, and read by nothing. user-id (userId) is wired into the widget's internal session provider, but no caller reads it: every own-message check uses user-chat-id instead. Both are inert today; treat them as reserved.
Boolean attributes (enable-ticket-sharing, demo) are true only when the value is exactly "true" or "". So enable-ticket-sharing="false" is correctly false, but ="1" and ="yes" are also false. Setting the same key through config takes the value as-is, so config.demo = true is the unambiguous form.
The config object is reactive, so changing any mapped attribute or reassigning config updates the live widget.
Methods
| Method | Signature | Behaviour |
|---|---|---|
open() | (): void | Opens the panel. In inline mode the rendering is unaffected, but it still sets the open state and emits gc:open, so an inline host's funnel records opens it never showed. No-op before the panel has mounted its callbacks. |
close() | (): void | Closes the panel. |
applyTheme(theme) | (Record<string, string>) => void | Builds a :host { … } rule into a dedicated adopted stylesheet, replacing the whole sheet on each call. See Theming. |
emitEvent(name, detail) | (string, object) => void | The public dispatch primitive. |
Events
Every event is dispatched with bubbles: true and composed: true, so it crosses the shadow boundary and reaches document.
js
el.addEventListener('gc:unread-count', (e) => {
badge.textContent = e.detail.count
})| Event | Detail | Emitted when |
|---|---|---|
gc:connected | { socketId: string } | Socket connected. |
gc:disconnected | { reason: string } | Socket dropped, or reason: 'reconnect_failed' after 10 attempts. |
gc:message-received | { chatroomId, message, fromUser } | A message arrived from someone else. Own echoes are reconciled, not re-emitted. |
gc:message-sent | { chatroomId, message } | Composer send, fired optimistically: including while offline, with status: 'failed'. |
gc:unread-count | { count: number } | Total unread across all rooms. |
gc:friend-added | { userId, username } | A friend request was sent, not when it was accepted. |
gc:open | {} | Panel opened. |
gc:close | {} | Panel closed, or close intent in inline mode. |
gc:tail-bet | { betId, selections } | The Bet action tapped on someone else's shared bet card. |
gc:error | { code, message, attributes? } | See the code table below. |
That is the whole catalogue, and it matches the ten events declared in the shipped custom-elements.json. A gc:navigate-tipster payload typedef survives in the source's internal JSDoc event map, but nothing dispatches it and it is not in the manifest or any published artefact. Do not build against it.
gc:error codes
| Code | Cause |
|---|---|
MISSING_ATTRIBUTE | In practice only socket-url absent. Carries an attributes array. The check runs inside the bootstrap, which requires both auth-token and api-url, so the api-url branch can never fire. See Configuration. |
SOCKET_CONNECT_ERROR | Handshake failed. |
SOCKET_RECONNECT_FAILED | 10 reconnect attempts exhausted. |
API_LOAD_FAILED | The initial lobby load (chatrooms + friends + notifications) failed, including auth rejections. Every other REST failure surfaces only as an in-widget toast and dispatches nothing. |
SOCKET_SEND_DROPPED | A send was attempted while the socket was down. |
CONVERSATION_CREATED_REFETCH_FAILED | Refetch after a new conversation failed. |
GROUP_JOIN_REFETCH_FAILED | Refetch after joining a group failed. |
The internal type union under-declares reality. The package's JSDoc GcErrorPayload['code'] lists only four of the seven: it omits SOCKET_SEND_DROPPED and both refetch codes. That union is source-internal and never published, so it cannot mislead your editor, but if you mirror it into your own types, mirror the full list above. Treat the code as a string rather than exhaustively switching on a fixed union.
Integrates with
This widget is not part of the goma:* cross-widget graph: it emits gc:* events and every pairing is host-mediated. Three relationships matter:
goma-live-chat: the other chat surface, sharing this widget's backend. They must also share one chat identity: exchange the operator session once and feed both. Both emitgc:tail-bet, so awindow-level listener receives events from either.goma-my-bets: embedded inside the share-a-bet screen and configured through themyBetsproperty. It is a declared dependency of this package and registers its own element, so you neither install nor import it separately. See Bet sharing.- The shared betslip:
gc:tail-betcarries the selections a viewer wants to copy; the host writes them into thegoma:betslip:v4envelope, which any mounted betslip widget picks up. See Shared state.
The widget reads and writes no shared-state envelope itself. It does keep one private localStorage key, for recently-used emoji.
Analytics and instrumentation
Every gc:* event is composed: true, so a single delegated listener on document captures the widget without holding an element reference:
js
const CHAT_EVENTS = [
'gc:open', 'gc:close', 'gc:message-sent', 'gc:message-received',
'gc:unread-count', 'gc:friend-added', 'gc:tail-bet', 'gc:error',
]
for (const type of CHAT_EVENTS) {
document.addEventListener(type, (e) => track(e.type, e.detail))
}The funnel
Engagement funnel
- Opened the dockgc:open. The earliest engagement signal; gc:connected fires earlier but is not user-initiated
- Had something waitinggc:unread-count with a non-zero count
- Received a messagegc:message-received. From someone else only
- Replied (good)gc:message-sent. Optimistic, see the traps below
- Grew their network (caution)gc:friend-added. Intent, not outcome
- Acted on a shared bet (note)gc:tail-bet
This widget has no phase event. goma-live-chat emits gc:phase-changed, which gives it a stage-by-stage funnel for free. The messenger has no equivalent: gc:open is the earliest signal, and everything before it (mounted, authenticated, connected) is invisible unless you derive it from gc:connected. Budget for a coarser funnel here.
Four traps in the data
gc:friend-added fires when a request is sent, not when it is accepted. Counting it as friendships formed will overstate your network growth by every request that was ignored or rejected. It measures intent, not outcome.
gc:unread-count fires on every inbound batch, whether or not the count actually changed: it is dispatched unconditionally at the end of each room handler. Deduplicate against the previous value before recording anything, or a busy conversation will flood your analytics with identical values.
gc:message-sent is optimistic. It fires before the server acknowledges, and it fires even while offline, with the message marked failed. A naive count of sends includes messages that never reached anyone.
Errors correlate with engagement, not just failure. A session that produced a gc:error is a session where someone was actually using the widget. Measure the cost (how many error sessions produced no subsequent gc:message-sent) rather than treating the error itself as the failure.
Detecting dead clicks
The messenger self-positions and owns its own open/close, so it does not have live chat's expand-echo problem. Its equivalent risk is the bubble colliding with other fixed furniture: a bubble sitting under a betslip FAB is tappable in theory and unreachable in practice. See Moving the bubble.
Empty / loading / error states
The widget renders nothing at all until it holds a token: no bubble, no panel, no placeholder. The one exception is demo, which bypasses the gate and renders the full UI with no token. Everything below assumes that gate has been passed.
| State | What the viewer sees |
|---|---|
| No token | Nothing. The widget is mounted but renders no bubble |
| Token present, connecting | The bubble, with the panel closed |
| Connected | Bubble plus, on open, the conversation lobby |
| Token cleared | Socket disconnects, the store resets, the panel closes and the navigation returns to the lobby |
A send attempted while the socket is down is dropped and reported as gc:error with code SOCKET_SEND_DROPPED; the message is not queued for retry.
Why a blank widget is hard to diagnose
Two failure modes produce no usable signal, and they are not symmetrical:
| Missing | What you get |
|---|---|
socket-url, with api-url present | gc:error with code: 'MISSING_ATTRIBUTE' and an attributes array: the one observable case |
api-url | Nothing. The validation that would report it sits inside a bootstrap that never runs |
auth-token | Nothing. No bubble, no panel, no event, no error |
Only one of the three is observable, and it is not the likely one: a token arrives asynchronously and can fail to arrive, while api-url is usually a constant. Detect the silent cases host-side:
js
// The dock only ever appears once a token lands.
setTimeout(() => {
if (!el.config.authToken) track('messenger_never_authenticated')
}, 5000)Bet sharing
Turn on enable-ticket-sharing and supply the myBets bridge. Note the attribute name differs from the sister widget: this one is enable-ticket-sharing, goma-live-chat uses enable-bet-sharing. Copying one onto the other silently disables the feature.
js
el.config = { enableTicketSharing: true }
el.myBets = { config: sportsbookConfig, sessionId, theme: myBetsTheme }The myBets object is opaque: the messenger never inspects it, it forwards the three fields verbatim onto the embedded <goma-my-bets> element's .config / .sessionId / .theme, and that widget is what actually renders the bet picker. Without the bridge the share screen shows a "not configured" state. The sessionId field is watched, so updating the object on login or logout keeps the picker in sync.
getTickets is near-dead, but not quite. The share screen no longer calls it: bet data comes from <goma-my-bets>. Its one remaining effect is that the composer's share button is enabled only when getTickets is assigned (!!element?.getTickets). So you still need to set it to something callable for the button to work, even though its return value is now ignored. This differs from goma-live-chat, where the property is fully inert and should be left unset:
js
el.getTickets = () => [] // presence gates the share buttonOlder ticket-sharing notes describe getTickets as the data source. It no longer is, but it still gates the button.
A bet shared by someone else renders as a card with a Bet action, which emits gc:tail-bet; your own shared bets show no tail action. To feed it into a betslip, write the selections into the shared envelope: see Shared state for the goma:betslip:v4 contract.
Forward bettingTypeId when you build ticket payloads. A tailed leg that reaches place-bet without bettingTypeId is rejected outright, and the whole slip fails, in the recipient's browser rather than the sender's. If you construct selections yourself, carry bettingTypeId and eventPartId through the round trip. See the same warning on goma-betslip-floating.
gc:tail-bet is emitted by both chat widgets. If you mount <goma-messenger> and <goma-live-chat> on the same page and listen on window, one handler receives both, and the payloads are not equally complete. Bind on the element, or disambiguate on event.target.
Layout and placement
In fixed mode (the default) the widget positions itself and the host needs no sizing:
| Element | Size | Position |
|---|---|---|
| Bubble | 56 × 56 | bottom: 20px; right: 20px: override with --goma-messengerFabBottom / --goma-messengerFabRight |
| Panel | 380 × 640, max-height: 85vh | bottom: 80px; right: 20px |
| Panel, at 767px and below | Full-screen | Fills the viewport |
In inline mode the host is display: block; width: 100%; height: 100%, so the parent must have an explicit width and height or the widget collapses. (The :host rule is unconditional, it applies in fixed mode too, but the bubble and panel are position: fixed, so nothing is left in the flow to give the host box height.)
Moving the bubble
Two custom properties exist specifically so the bubble can clear a host's own floating furniture. They are declared in the widget's stylesheet with these defaults and read with matching inline fallbacks, so either path gives the same result:
| Token | Default |
|---|---|
--goma-messengerFabBottom | 20px |
--goma-messengerFabRight | 20px |
The bubble's colour can move out of the way too: with bubble-variant="secondary" it paints --goma-secondaryFloater (no stylesheet default; falls back to --goma-highlightSecondary) instead of the primary accent, so it does not read as a second copy of a host's primary floating button.
js
el.style.setProperty('--goma-messengerFabBottom', '96px')This matters when stacking against other floating widgets: goma-betslip-floating has its own bottom offset for the same purpose, and a live-chat dock occupies the same corner. Derive all the offsets from one set of constants so they cannot drift apart.
Hide the bubble rather than offsetting it around a full-screen surface. When something covers the corner entirely (an expanded live chat, for instance) no offset keeps the bubble clear, and a second chat button floating over an open chat is just noise. Set display: none for the duration.
:host carries z-index: 2147483647. The maximum signed 32-bit integer, so the messenger sits above essentially everything. Override it by setting z-index directly on the <goma-messenger> element.
Theming
Tokens use the shared --goma-* prefix, the same one described in the theming guide. The names below are this widget's own set (the guide's catalogue covers the sports-domain family) but the mechanism, the prefix and the normalisation rules are common.
js
// Imperative
el.applyTheme({ '--goma-backgroundPrimary': '#1b1d2b', '--goma-highlightSecondary': '#29f2ff' })
// The same thing through the property
el.theme = { backgroundPrimary: '#1b1d2b' }
// Declarative, as a JSON string
el.setAttribute('theme', JSON.stringify(theme))
// A shipped preset
import { exclusivebetTheme } from '@gomagaming/messenger'
el.applyTheme(exclusivebetTheme)applyTheme() accepts a key in any of three forms, bare (backgroundPrimary), ---prefixed (--backgroundPrimary), or fully qualified (--goma-backgroundPrimary), and normalises them all to the last. Each call replaces the whole theme sheet rather than patching it, so pass the complete set you want painted. Removing the theme attribute empties that sheet and reverts to the defaults.
Retired --gc-* keys are dropped, not renamed
A key still carrying the old --gc-* prefix is not a form to normalise (it is silently invalid CSS) so it is discarded rather than applied under a mangled name. Every dropped key is named in a single console.warn per applyTheme() call. The warning fires in every build, not just development: the shared build preset hard-defines NODE_ENV as production, so a dev-only guard would be stripped from the published bundle and never reach the host it exists to help.
Custom properties inherit through the shadow boundary, so plain host-page CSS works too:
css
goma-messenger { --goma-backgroundPrimary: #1b1d2b; }Specificity beats order here, and that is widget-specific. applyTheme() and clientstyling both emit a plain :host rule, which :host(.gc-light) in the widget's own stylesheet out-specifies. So on an element carrying class="gc-light", the light theme wins for every token it defines, whatever order you applied things in: target :host(.gc-light) in your own CSS to override it. (Its sister goma-live-chat resolves this differently and does not share the behaviour; do not carry a conclusion from one page to the other.) Between theme and clientstyling there is no fixed order: each sheet is appended on first use, so whichever you apply second wins.
Three presets are exported from the package entry:
| Preset | Style |
|---|---|
exclusivebetTheme | Exclusivebet brand colours: dark charcoal with a gold accent |
betssonfranceTheme | Betsson France brand colours |
betathomeTheme | bet-at-home brand colours: a light palette (white / navy / lime) |
A built-in light theme is available by putting class="gc-light" on the element itself: the class name kept its gc- prefix through the token rename. There is no appearance concept and no prefers-color-scheme support; the default is dark.
The light theme is partial. It overrides 34 of the 59 tokens. Still at their dark values in light mode: the alert trio (--goma-alertSuccess / --goma-alertError / --goma-alertWarning), --goma-chatBorderElevated, --goma-chatTimestamp, --goma-inputPlaceholder, both --goma-chatBet* tokens, both --goma-liveTag* tokens and all three --goma-myTickets* tokens. If you need a genuine light presentation, supply those explicitly.
Tokens used by this widget
All 59, exactly as declared in src/styles/variables.css. Every one of them is read by the widget's own CSS: there are no inert tokens in this set.
| Token | Default | Used for |
|---|---|---|
--goma-backgroundPrimary | #091737 | Chat panel background |
--goma-backgroundSecondary | #0C2251 | Header / footer background |
--goma-backgroundTertiary | #0E2B65 | Secondary panel background |
--goma-backgroundCards | #0E2B65 | Card background |
--goma-chatBorderElevated | #11367F | Elevated surface border |
--goma-chatBubbleMine | #533BFF | Own message bubble |
--goma-chatBubbleOther | #0C2251 | Other message bubble |
--goma-highlightPrimary | #C0CE00 | Primary accent: bubble, CTAs, friend code |
--goma-highlightSecondary | #533BFF | Secondary accent |
--goma-chatHighlightSecondary | #533BFF | Secondary accent, chat surfaces |
--goma-highlightTertiary | #83FF4A | Tertiary highlight |
--goma-alertSuccess | #2BDA3F | Success states |
--goma-alertError | #FF6767 | Error states, unread badge |
--goma-alertWarning | #f59e0b | Warning states |
--goma-textPrimary | #FFFFFF | Primary text |
--goma-textSecondary | #ADE4FF | Secondary text |
--goma-allDark | #091737 | Text on accent surfaces |
--goma-chatBubbleTextMine | #FFFFFF | Own bubble text |
--goma-chatBubbleTextOther | #FFFFFF | Other bubble text |
--goma-inputTextTitle | #ADE4FF | Muted text: labels, hints, placeholders |
--goma-chatTimestamp | #ADE4FF | Message timestamps |
--goma-textDisablePrimary | rgba(255,255,255,0.28) | Section headers, disabled text |
--goma-buttonBackgroundPrimary | #C0CE00 | Primary button |
--goma-buttonTextPrimary | #091737 | Primary button text |
--goma-buttonActiveHoverPrimary | #a8b400 | Primary button, hover / active |
--goma-buttonDisablePrimary | rgba(192,206,0,0.4) | Primary button, disabled |
--goma-buttonBackgroundSecondary | #533BFF | Secondary button |
--goma-buttonTextSecondary | #FFFFFF | Secondary button text |
--goma-buttonTextTertiary | rgba(255,255,255,0.48) | Ghost button text |
--goma-buttonBorderTertiary | rgba(255,255,255,0.12) | Ghost button border |
--goma-inputBackgroundSecondary | #0E2B65 | Input background |
--goma-inputBorderActive | #C0CE00 | Focused input border |
--goma-inputText | #FFFFFF | Input text |
--goma-inputPlaceholder | #FFFFFF | Input placeholder |
--goma-iconPrimary | #FFFFFF | Primary icons |
--goma-iconSecondary | rgba(255,255,255,0.48) | Secondary icons |
--goma-chatIconGroup | #ADE4FF | Group avatars |
--goma-separatorLine | #143F93 | Borders |
--goma-separatorLineSecondary | #143F93 | Separators |
--goma-radiusPill | 22px | Pill radius: the only shape token in this set |
--goma-chatBetLegBackground | #0C2251 | Bet-card leg background |
--goma-chatBetScore | #C0CE00 | Bet-card leg score |
--goma-liveTag | #C0CE00 | LIVE chip background |
--goma-liveTagText | #091737 | LIVE chip text |
--goma-myTicketsWon | #0F5531 | Won status |
--goma-myTicketsLost | #60111B | Lost status |
--goma-myTicketsOther | #946F00 | Other / partial status |
--goma-scroll | rgba(255,255,255,0.08) | Scrollbar thumb |
--goma-fontFamily | 'Exo 2', system-ui, -apple-system, sans-serif | Typeface |
--goma-fontSize2xs | 9px | Font size: 2xs |
--goma-fontSizeXs | 10px | Font size: xs |
--goma-fontSizeSm | 12px | Font size: sm |
--goma-fontSize | 14px | Font size: base |
--goma-fontSizeLg | 16px | Font size: lg |
--goma-messengerFabBottom | 20px | Bubble offset from the bottom |
--goma-messengerFabRight | 20px | Bubble offset from the right |
--goma-chatShadowSm | 0 1px 2px rgba(0,0,0,0.2) | Small shadow |
--goma-chatShadowMd | 0 4px 6px -1px rgba(0,0,0,0.3) | Panel shadow |
--goma-transitionNormal | 250ms ease | Transition duration |
One more token is read but not declared in variables.css, so it has no stylesheet default:
| Token | Default | Used for |
|---|---|---|
--goma-secondaryFloater | none (falls back to --goma-highlightSecondary) | Bubble fill when bubble-variant="secondary" |
The same 59 keys plus --goma-secondaryFloater (60 in all), with labels and grouping for tooling, are exported from the package's src/themeTokens.js registry. Four default values there are #RRGGBBAA conversions of the rgba() forms above (textDisablePrimary, buttonDisablePrimary, iconSecondary, scroll), --goma-secondaryFloater carries no default (its fallback is --goma-highlightSecondary), and the twelve non-colour entries (the radius, the font stack and the five font sizes, the two FAB offsets, the two shadows and the transition) carry no default at all. Where the two disagree, the stylesheet is what the widget paints.
For arbitrary CSS beyond the tokens, use the clientstyling / clientstylingurl attributes, targeting :host.
i18n / message overrides
Five locales ship in the bundle: en, pt, fr, tr, da. Set the active one with the locale attribute, which is reactive at runtime. An unknown locale falls back to en.
Message namespaces are chat, friends, group, ticket, common, emoji and aria.
tr and da are incomplete. Both are missing the emoji namespace that en, pt and fr carry, so every string in the emoji picker (its label, search placeholder, category names and "recently used" heading) falls back to English in those two locales. Supply the gap via translationurl if that matters to you.
To add or override strings, point translationurl at a JSON bundle shaped as { "<locale>": { …messages } }. Fetch failures log a warning and emit no gc:error.
translationurl is dropped if set before the element connects
The fetch handler returns early when i18n has not been created yet, and nothing retries. Set the attribute after the element is in the DOM. The merge is also one-way: removing the attribute later does not restore the bundled strings.
Composer limits
Messages are hard-capped at 150 characters. This is not configurable, and it is considerably shorter than the 500-character limit on goma-live-chat: do not assume the two chat surfaces accept the same message length.
goma-messenger150 characters
goma-live-chat500 characters
The limit is enforced with the input's own maxlength, so further input is blocked at the ceiling rather than the message being rejected on send, and a counter is shown throughout rather than only near the ceiling. A constraint revealed only at the moment of rejection is one of the most reliable ways to lose someone mid-task; this one is visible from the first character.
The character counter is hidden from assistive technology. The counter carries aria-hidden="true", so a screen-reader user gets no warning as they approach the limit and no explanation when input silently stops being accepted. Until that changes, consider announcing the remaining count yourself in a polite live region if your audience needs it.
Embedding examples
Vanilla HTML
html
<goma-messenger id="chat"></goma-messenger>
<script type="module">
import '@gomagaming/messenger'
const el = document.getElementById('chat')
el.applyTheme({ '--goma-backgroundPrimary': '#1b1d2b' })
el.myBets = { config: sportsbookConfig, sessionId, theme: myBetsTheme }
el.getTickets = () => []
el.config = {
apiUrl: '/goma-chat',
socketUrl: '/',
position: 'fixed',
userChatId: chatUserId,
username,
userCode: friendCode,
enableTicketSharing: true,
socketAuthToken: chatToken,
authToken: chatToken,
}
</script>React
jsx
import '@gomagaming/messenger'
function Messenger({ token, chatUserId, username, friendCode, theme }) {
return (
<goma-messenger
ref={(el) => {
if (!el) return
el.applyTheme(theme)
el.config = {
apiUrl: '/goma-chat',
socketUrl: '/',
userChatId: chatUserId,
username,
userCode: friendCode,
socketAuthToken: token,
authToken: token,
}
}}
/>
)
}Re-applying the same token is a no-op, so a re-render is harmless; but any change to auth-token forces a full reconnect, so derive it from state rather than regenerating it per render.
Vue 3
Tell the compiler the tag is a custom element, in vite.config.js:
js
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('goma-') } } })Attributes can then be bound declaratively, and events use the literal name:
vue
<script setup>
import '@gomagaming/messenger'
import { ref, watch } from 'vue'
const el = ref(null)
function setEl(node) {
if (!node) return
node.applyTheme(theme)
node.getTickets = () => []
node.myBets = { config, sessionId: session.sessionId, theme: myBetsTheme }
}
watch(() => session.sessionId, (sid) => {
if (el.value) el.value.myBets = { config, sessionId: sid, theme: myBetsTheme }
})
</script>
<template>
<goma-messenger
v-if="session.chatToken"
:ref="setEl"
api-url="/goma-chat"
socket-url="/"
position="fixed"
locale="en"
:auth-token="session.chatToken"
:socket-auth-token="session.chatToken"
:user-chat-id="session.chatUserId"
:username="session.username"
:user-code="session.chatFriendCode"
enable-ticket-sharing="true"
@gc:unread-count="onUnread"
/>
</template>Angular hosts need CUSTOM_ELEMENTS_SCHEMA on the module.
Local development
Because api-url is a relative path, a dev server needs to proxy it. In vite.config.js:
js
server: {
proxy: {
// The prefix is stripped: the chat service serves /social/v1/..., not
// /goma-chat/social/v1/...
'/goma-chat': {
target: process.env.CHAT_API_TARGET,
changeOrigin: true,
secure: true,
rewrite: (p) => p.replace(/^\/goma-chat/, ''),
},
// Separate entry: Socket.IO serves its transport from the origin's
// /socket.io/ path, never under the REST prefix. `ws: true` is required
// or the upgrade never completes.
'/socket.io': { target: process.env.CHAT_API_TARGET, changeOrigin: true, secure: true, ws: true },
},
}Miss the rewrite and every call 404s; miss ws: true and the socket silently falls back to failing long-polls.
Deployment notes
The reference host proxies the chat service same-origin so the widget's api-url is a relative path and the browser never faces the chat host's CORS.
Point the socket at the origin, not at the REST path
Socket.IO parses the path component of the URL you give it as a namespace, so the widget cannot be pointed at /goma-chat for its socket. It takes socket-url="/" and connects to the well-known /socket.io/ path at the origin, which must be proxied separately.
Two nginx details fail in confusing ways if you get them wrong:
- Do not hardcode
Connection: upgrade. Socket.IO opens with HTTP long-polling and only then upgrades, so a polling request carrying noUpgradeheader would be mangled. Map the header so it echoesupgradeonly when the client asked for one, and raiseproxy_read_timeoutwell above the 60 s default so idle chats are not dropped. - A missing REST location fails invisibly. Without a
/goma-chat/block the path falls through to an SPA catch-all, which answersindex.htmlwith a 200. The client'sJSON.parsethen fails and the messenger sits silently signed out, which looks like a chat outage rather than a routing mistake.
The font is injected as a <link> into document.head, not into the shadow root: deliberately, because Firefox ignores @font-face inside shadow roots. It is deduplicated across instances (keyed on the URL, via a data-goma-font attribute) but never removed on unmount. Set font-url="none" to opt out.
Content-Security-Policy needs style-src 'unsafe-inline' (for the <style> fallback and the widget's inline style attributes, not for the adopted sheets, which are constructed and exempt) plus connect-src for the REST and socket origins. The default font needs both style-src https://fonts.googleapis.com and font-src https://fonts.gstatic.com, because font-url points at a Google Fonts stylesheet; font-src alone blocks it before gstatic is ever reached. Set font-url="none" to avoid both.
adoptedStyleSheets requires Chrome 73, Edge 79, Firefox 101 or Safari 16.4. There is a <style> fallback, so older engines still render.
Accessibility
Read off the current component tree. The message feed is a role="log" with aria-live="polite", so incoming messages are announced. The modal component is role="dialog" aria-modal="true" and the emoji picker is role="dialog"; the emoji picker's category strip is a role="tablist" of role="tab" elements carrying aria-selected. The loading spinner is role="status" and the toast is role="alert". Interactive controls are broadly labelled: 47 aria-labels across the tree, resolved through the aria namespace in each locale file, so they translate with the locale attribute.
Motion is preference-aware. The widget animates a fair amount: the panel unfolding out of the bubble, messages sliding in, the unread pulse ring and badge bounce, the quick-react pop-in, the modal fade-and-scale, the emoji toggle and the empty-state dots, and every one of those is neutralised under prefers-reduced-motion: reduce, both by a blanket rule in the widget's stylesheet and by targeted animation: none / transition: none overrides.
Worth testing yourself before you rely on it: the conversation list and the chat panel carry no tab or dialog semantics, so neither is announced as a navigable structure; focus management on open and close is unverified; only a handful of decorative glyphs carry aria-hidden, so the rest are left to the reading order; and the character counter is hidden from assistive technology, as noted under Composer limits.
Integration Caveats
SSR/webview. This package doesn't use the shared
createWidgetElementfactory (it declares its ownHTMLElementsubclass directly), but had the identical hazard: importing it outside a browser previously threw immediately. Fixed with the same guard pattern.Configuring before the definition loads. Assigning
config(or any property) while the element is still an unknown element is safe: the value is replayed through the widget when it upgrades. That matters when loading from the CDN, whose loader imports widget modules asynchronously, so your script usually runs first. On release sets at 0.34.0 and earlier the value was silently discarded and the widget mounted with no configuration and a clean console: there, awaitwindow.__gomaCdn.readyfirst, or configure through attributes. See Setting a property before the widget's definition has loaded.
See Integration Caveats for the cross-widget picture.
Changelog
Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.
Every release that changed this widget, newest first. The latest is open; the rest expand.
0.37.6
<goma-my-bets>now offers cash out on open bets. A player can cash out the whole bet or only part of the stake, and sees the cash out value update live. Cash out is on by default. SetcashoutEnabled(attributecashout-enabled) tofalseto turn it off. The widget emits two new events:goma:cashout-success(withbetId,cashoutType,cashoutAmount,pendingand, for a partial cash out,partialCashoutStake) andgoma:cashout-error(withbetId,cashoutType, areasonofnot-signed-in,invalid-stake,network,http-error,rejectedorunknown, and, when known, the HTTPstatusand the provider'smessage). Three new image properties,myBetsCashoutWarningImage,myBetsCashoutSuccessImageandmyBetsCashoutErrorImage, set the pictures on the confirm, success and error steps. When one is not set, that step shows no picture.A cash out counts as done only when the provider answers with
success: true.cashoutAmountis the amount the provider paid, or the quoted value when it sends none. When the provider accepts a cash out but holds it for approval,pendingistrue, the bet stays in the Open list and the list is fetched again.<goma-betslip-sidebar>and<goma-betslip-floating>forward the newmyBetsCashoutEnabledattribute (my-bets-cashout-enabled) and the three image properties to the My Bets tab. After a successful cash out, the betslip asks for a new balance at once and again after 2.5 seconds. In<goma-betslip-floating>, the cash out dialog can open over the betslip dialog, and closing it leaves the betslip open and does not move the page behind.<goma-live-chat>and<goma-messenger>turn cash out off in the My Bets list of their ticket share screens.@gomagaming/network:openSseaccepts amethodoption and abodyoption, so a stream can be opened with a POST request.openSsealso callsonClosewhen its retries run out after an error, so a consumer knows the stream is finished.@gomagaming/my-betsholds the cash out dialog logic and now depends on@gomagaming/ui.<goma-my-bets>has a fourth sub-tab, Cash Out, that lists fully cashed out bets only (a partly cashed out bet stays in Open). It shows only while cash out is on, a full cash out that is not pending moves the widget to it when the success step closes (a partial or pending one shows Open), a page that holds only hidden bets loads the next page by itself, andgoma:my-bet-selectcan now reporttab: 'cashout'. A typed partial amount is rounded down to cents, and Try again shows the current value before the player confirms. After a partial cash out, the card shows a Cashed Out tag next to the title and a box with the amount paid out and the stake still in play; the Bet Amount keeps the stake as placed. A fully cashed out card keeps the neutral look, shows no leg result and a box with the total paid out. Possible Winnings is always the provider's figure (the winnings without the stake on a free bet). The cash out dialog is centred on every screen size, up to 480px wide, the amount field shows the currency, and the ? opens a tooltip.@gomagaming/sports-domainhas the cash out API calls and the live value stream. The stream keeps the last values while it retries and opens again when the browser goes back online or the tab becomes visible. It exports two new composables,useMyBetsCashoutValuesanduseMyBetsCashout, and a newrequestBalanceRefreshaction on the user store.@gomagaming/coreand@gomagaming/uicarry the shared dialog and slider styles.GomaDialogin@gomagaming/uitakes an optionalpanelClasswith extra classes for the panel; when set, it also replaces the default border colour. In@gomagaming/core,useScrollLocknow does nothing for a lock that is taken while another lock is active.<goma-betslip-floating>now closes the dialogs that are open inside it (the bonus picker too) before it closes itself.<goma-my-bets>has a newsubTabproperty (attributesub-tab). The host sets which sub-tab opens,'open','settled'(or'closed'),'won'or'cashout', and can change it later. The player can still click any tab.<goma-betslip-sidebar>and<goma-betslip-floating>have a newmyBetsSubTabproperty (attributemy-bets-sub-tab) that sets the sub-tab inside the My Bets tab.cashoutEnabledon<goma-my-bets>andmyBetsCashoutEnabledon both betslips turn cash out off when the property is set to the string'false', the same as the attribute.
0.37.3
Fix bet placement with the "Do not accept any changes in odds" setting. The betslip sent
oddsValidationType: 'ACCEPT_NONE', which is not a value the provider accepts, so every bet failed with error 147 "Invalid Odds Validation Type". It now sendsWITHIN_THRESHOLD, one of the three values the provider accepts. The provider then refuses any odds change when the account threshold is 0.Hosts in
placeBetMode: 'event'see the change too: thegoma:place-bet-requestdetail now carriesoddsValidationType: 'WITHIN_THRESHOLD'for this setting, where it carried'ACCEPT_NONE'before. A host that forwards the payload to its own backend, or checks this value, must acceptWITHIN_THRESHOLD. Thecustom-elements.jsontype of that field now lists it.Two more place-bet changes ride in the same release, and one display fix.
The place-bet payload now sends
freeBetas a boolean:trueonly when theubsWalletIdon that payload belongs to a free-bet wallet (so the provider books a free bet and not a cash stake),falseotherwise, where it sentnullbefore; thegoma:place-bet-requestdetail type changes fromnull|objecttoboolean. A betslip pick now setsubsWalletIdandfreeBettogether and wins over a hostplaceBetBody.freeBet; with no betslip pick a host that sends a free-bet wallet throughplaceBetBody.ubsWalletIdmust also sendfreeBet: true. The picked wallet's bonus type is now stored with the pick (and shared like the wallet id) as{ walletId, type }inselectedBonusTypeandselectedBonusTypeBySelectionof thegoma:betslip:v4envelope, and is used only for that same wallet id. A free bet pick on a Single selection stays flagged as a free bet, and is no longer cleared, when a refresh of that selection's wallets response leaves the wallet out. The two fields are additive and optional, so the envelope version does not change. In the same event detail type,ubsWalletIdis now absent instead ofnullwhen no bonus is picked, andoddsBoostPercentandcapAmountare listed.A Single bet can now be placed with an odds boost: the selection picker lists a boost that has reached a stair (it has a rate and a cap), leaves the typed stake alone, and the selection's payload carries the wallet id with the boost terms read from that selection's own wallets response. A boost with no usable stair is not offered on a Single selection, for fetched and host-fed wallets alike.
The betslip, My Bets and the chat ticket share now show a free bet's winnings without the stake. My Bets reads
potentialNetReturns(or stake times odds minus one) for every status except lost (0) and cashed out (unchanged), where it showed the stake as part of the winnings. The betslip shows the quote minus the stake for a Single selection or a Multiple slip carrying a free bet. ThepossibleWinningsofgoma:bet-placedkeeps the provider's figure, stake included. The chat ticket (possible_winnings) follows the same rule for a free bet.For hosts: in
goma:place-bet-requestand in the place-bet body,freeBetis now a boolean, and a betslip pick sets it. A host that used to put a free-bet token object inplaceBetBody.freeBetmust stop doing that. SendfreeBet: trueonly when you send your own free-bet wallet id and the betslip has no pick.@gomagaming/sports-domainhas two new exports:isFreeBetWallet(whether a wallet id belongs to a free-bet wallet) andfreeBetNetWinnings(a free bet's winnings without the stake).
0.36.1
Playground manifests (
@gomagaming/<widget>/playground) now offer a control for every setting each widget really has. Settings that cannot be a form field (raw provider data, read-only state mirrors, props a widget never reads) are recorded with a reason in the repo instead of being silently missing.Controls that never reached the widget are fixed:
- The betslips'
placeBetHeadersJson/placeBetBodyJson/quickAddAmountsJsonare nowplaceBetHeaders/placeBetBody/quickAddAmounts. - The time-filter and filters-settings ticks editors set
ticks. - live-chat's layout control writes
position, the config key the element reads. - The sports-navigation widgets drop the unread
sportsJson. - JSON-typed defaults are real objects and arrays instead of serialized strings. The top-competitions
highlightedImagesstring default was indexed character by character and requested bad image URLs on every load. The missions-hubemptyStateImagesillustrations now show.
No widget runtime behaviour changes.
- The betslips'
0.36.0
Add a
bubble-variantattribute ('primary'|'secondary', default'primary') so a host can pick which floater the chat bubble uses.secondaryis the 48px secondary floater (the primary one is 56px), with a white icon, for hosts whose primary floater is already taken: for example by a mobile betslip button.The secondary bubble fill now reads
--goma-secondaryFloater, falling back to--goma-highlightSecondary(no visual change unless a host sets it), so a host can colour the floater without recolouring every in-chat highlight.The option is available as the
bubble-variantattribute and thebubbleVariantproperty (same state), and as a knob in the playground. The docs page lists it next topositionand documents--goma-secondaryFloater, which has no stylesheet default and falls back to--goma-highlightSecondary.
0.35.0
Match dates read day-first in English, so one match is no longer written two different ways across the suite.
An event card writes the kick-off as a compact
11/10;<goma-game-details>'s header,<goma-hero-card>,<goma-multi-banners>and<goma-search>'s result rows write it in words throughtoLocaleDateString. Withlocale: 'en'(the bare tag, whichIntlresolves to American ordering) that long form came out asOctober 11next to the card's11/10. English was the only shipped locale that disagreed with its own cards (frandptare already day-first, and the shared time formatter has always been pinned toen-GB), so the long form now resolves a bareentoen-GB. A host that explicitly configuresen-USkeeps American ordering.Two surfaces build their own
Intl.DateTimeFormatrather than going through the shared formatters, and both showed the same inversion for the same match: the My Bets card's leg date (which also printed07 Octoberwhere the cards print7 October, and sat directly under a hand-builtdd/mm/yyyy"placed" line) and the live-chat bet modal's per-leg date. Both now resolve the locale the same way.Every package listed here renders a date that changes, which is why the note reaches all of their pages rather than only the two whose source moved: including the two betslips and
<goma-messenger>, which render the same leg date through the<goma-my-bets>widget they embed.Not covered, and deliberately: the missions surfaces format challenge dates with the browser's locale rather than the widget's, and, in the same file, render the same instant as a deliberately locale-independent
DD/MM/YYtwo functions below, so one view contradicts itself regardless of locale. That is a different defect from this one and wants its own ticket.
0.29.1
Make every text surface in the chat widgets honour
--goma-fontFamily.Eight declarations across the two widgets hardcoded a typeface and so could not be reached by a host theme at all:
<goma-live-chat>: the three "Chat Closed" hero rules (final score, title, subtitle) namedInter. That face is not one the widget ships: itsfontUrlloads Exo 2, so those three have been falling back to the generic sans-serif rather than rendering as intended.<goma-messenger>: five inline styles across the friends screens named'Exo 2'directly. Inline styles also outrank the adopted stylesheet, so a host that set--goma-fontFamilysaw it apply everywhere except here.
All eight now resolve through
var(--goma-fontFamily), which both widgets already declared in their theme-token registries. A host that sets that token (alongsidefont-url="none"to suppress the widget's own font injection) now gets one typeface across the whole widget.Themes that relied on those surfaces staying Inter or Exo 2 while the rest of the widget changed will now follow the token; set
--goma-fontFamilyto the face you want.
0.29.0
Integration caveats: fixes from a cross-widget review of every shipped widget from an integrator's point of view.
Cross-cutting fixes:
@gomagaming/core'screateWidgetElement()factory, andlive-chat's andmessenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): theHTMLElementsubclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw aReferenceErrorimmediately.
Per-package fixes:
favorites: addedsrc/themeTokens.jsand a:hoststylesheet (previously the package had neither, so it silently rendered core's base theme instead of its documented dark-navy/cyan/amber theme).suggested-bet. Fixed a leaked WAMP subscription: switching a mounted widget'sdataSourceaway fromrpc/subscribenever released the open topic.filters-settings: fixed a dead:host{display:inline-block}CSS rule (the shared element factory's inlinedisplay:blockalways won), so the widget always rendered full-width instead of the intended compact inline trigger.game-details: added amin-heightfallback for the inert/no-eventIdstate in an unsized container.betslip-sidebar,my-bets,competition-filter,suggested-bet,events-horizontal,top-competitions,top-competitions-pills,competitions-list,multi-banners,casino-games,sports-navigation-horizontal. CorrectedthemeTokens.jsregistries that had drifted from actual token usage, added missinggoma:retryEvents-table rows, and other small doc/registry corrections.region-filter: added a missingtypeof window !== 'undefined'guard on a window listener.live-chat: correctedcustom-elements.json'smembersarray (missingtheme/configaccessors, and an inaccurate description).sports-domain: corrected a stale code comment that seeded a documentation error in two Missions widgets.
Three fixes (
favorites,game-details,filters-settings) change real rendering and are flagged indocs/guide/integration-caveats.mdas needing human visual verification before release.
0.27.0
Fixes to both chat widgets, found while bringing them onto the monorepo's conventions. All of these are longstanding: they predate the move and were present in the standalone packages.
Component CSS that never rendered now renders. Ten components carried Vue
<style scoped>blocks. Scoped styles compile into a stylesheet the widgets never load, so none of it had ever reached a user: the message slide-in, the jump-to-latest pill, the chat bubble's hover and press feedback, its unread pulse ring, the unread badge bounce, the quick-react pop-in, the conversation-list loading dots, the modal fade-and-scale, the emoji toggle's hover and active states, and a scrollbar narrower than the default. These are now part of each widget's stylesheet and all of them honourprefers-reduced-motion. Neither package emits adist/style.cssany more: that file only ever contained the unreachable rules.Font sizes were being applied as colours. Ten Tailwind utilities used
text-[var(--goma-fontSize…)], which compiles tocolor:rather thanfont-size:, so the size was silently dropped. On both text inputs the bogus rule also overrode the input's actual text colour. Buttons, inputs, the reconnecting banner and the jump-to-latest pill are affected.live-chat: the webfont now loads where Firefox can see it. The
font-urlstylesheet was injected into the shadow root, and Firefox ignores@font-facedeclared there. The widget silently fell back to a system font. It now goes todocument.head, deduped and shared across instances, matching what messenger already did.live-chat: logging out closes the socket. Clearing
auth-tokenemptied the UI and reset the store while leaving the Socket.IO connection open, still presenting the old token, until the element left the DOM.live-chat: bodyless requests no longer claim to carry JSON. The join and leave calls sent
Content-Type: application/jsonwith a zero-length body, obliging every backend to special-case an empty payload it had been told to parse.live-chat:
betathomeThemeis now exported. The preset shipped in the package but nothing exported it, so it could not be reached from outside. Both widgets now export the same three presets.messenger: the conversation list loads in full. It read one page from a paginated endpoint and discarded the cursor, so anyone with more conversations than the server's page size simply never saw the rest. No error, and no way to reach them. This also fixes
createGroup()and adding a friend by code returningnullfor those users: both look for the new conversation in a reloaded list, and it could be on a page that was never fetched.Both: the retired
--gc-*theme-key warning actually fires. It was gated on a development check that the production build always compiles away, so the keys were dropped in silence. Exactly the behaviour the warning existed to prevent. It now reports every dropped key once perapplyTheme()call.Live chat and messenger are now part of the GomaPulse monorepo and release on the shared version line.
@gomagaming/core's ambient type declarations now registergoma-live-chatandgoma-messengeralongside the rest of the custom elements, so consumers get the same JSX/Vue template type-checking for these two widgets that every other widget already has.@gomagaming/livechathas been renamed to@gomagaming/live-chat, so its directory, package name and<goma-live-chat>element tag all agree. Update the dependency name and the import specifier; no attribute, event or theming API has changed.@gomagaming/messengerno longer bundles@gomagaming/my-bets; it depends on the published package and imports it, so a page running several Goma widgets ships one copy of it instead of one per bundle.<goma-live-chat>already worked this way. No host action is needed to load or register<goma-my-bets>(it is a declared dependency and registers itself) but because it reaches@gomagaming/coreand@gomagaming/sports-domain, both chat packages now require the usual peer install (vue,pinia,vue-router,vue-i18n,@vueuse/core) at the host-app level.<goma-messenger>is now built like every other GomaPulse widget.Theme tokens have been renamed from
--gc-*to--goma-*. Any host passing theme overrides. Through thethemeattribute, the.theme/.config.themeproperties,applyTheme(), or the exported presets (exclusivebetTheme,betssonfranceTheme,betathomeTheme). Must update its keys. The widget now warns, naming every key dropped, because an unrecognised custom property is otherwise silently ignored. Colours are unchanged: every value was pinned during the migration, so the widget looks identical.Vue, Pinia and vue-i18n are no longer bundled. Install them alongside the widget:
pnpm add @gomagaming/messenger vue pinia vue-router vue-i18n @vueuse/core. In return the bundle is far smaller, and a page running several GomaPulse widgets now loads one copy of the framework instead of one per widget.The package no longer ships TypeScript declarations, matching the rest of the widget family: its source and tests are now plain JavaScript.
Internal layering flattened. The clean-architecture split (
usecase/,data/mapper/, session-provider abstractions, …) has been collapsed into the same flatcomponents//composables//stores//utils//api/shape every other widget uses. The public surface (the<goma-messenger>tag, its attributes, theconfig/theme/myBetsproperties, and everygc:*event) is unchanged. Only a host reaching past the package entry point into one of the moved internal paths is affected.@gomagaming/live-chatgains a.themeproperty accessor alongside its existing.config: reading it returns the--goma-*custom properties the widget actually painted (not a mirror ofconfig.theme); assigning routes throughapplyTheme(), with the same key-normalisation and retired---gc-*warning described above. No attribute or event changed.