Skip to content

<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/core

The 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 state

myBets, 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

  1. Connection attributes
    api-url, socket-url, api-key
  2. Identity attributes
    user-chat-id, username, user-code, socket-auth-token
  3. 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 nothing

Older 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:

FailureSignal
REST rejects a callgc:error { code: 'API_LOAD_FAILED' }: the 401 is flattened into the message string
Socket handshake rejectedgc:error { code: 'SOCKET_CONNECT_ERROR' }
10 reconnects exhaustedgc: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.

MethodPath
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:

EventPayload
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:

EventPayload
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 }
notificationfriend 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.

AttributeConfig keyDefaultDescription
api-urlapiUrl''REST base URL. Required and validated.
socket-urlsocketUrl''Socket.IO server URL. Required and validated.
auth-tokenauthToken''Bearer token. Gates rendering and drives bootstrap.
socket-auth-tokensocketAuthTokenundefinedToken sent as the socket auth.token. Not validated.
user-chat-iduserChatId''Identifies the viewer's own messages. Not validated.
usernameusername-Sender name on optimistic sends.
user-codeuserCode-The viewer's friend code, shown and copyable in Add Friends.
user-avataruserAvatar-Avatar URL.
localelocale'en'One of en, pt, fr, tr, da.
positionposition'fixed''fixed' (bubble plus panel) or 'inline'. Any other value is treated as fixed.
bubble-variantbubbleVariant'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-sharingenableTicketSharingfalseEnables 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.
themetheme{}JSON string of --goma-* overrides. See Theming.
icon-base-urliconBaseUrl-Base URL for icon assets.
api-keyapiKey-Sent as x-api-key.
font-urlfontUrlExo 2 on Google FontsSet to 'none' to disable font injection.
demodemo-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.
PropertyDefaultDescription
configresolved defaultsGetter/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.
myBetsundefined{ config?, sessionId?, theme? }, forwarded verbatim to the embedded <goma-my-bets>.
getTicketsundefinedSee the note under Bet sharing.
getMockTipsundefinedDemo-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 ​

MethodSignatureBehaviour
open()(): voidOpens 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()(): voidCloses the panel.
applyTheme(theme)(Record<string, string>) => voidBuilds a :host { … } rule into a dedicated adopted stylesheet, replacing the whole sheet on each call. See Theming.
emitEvent(name, detail)(string, object) => voidThe 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
})
EventDetailEmitted 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 ​

CodeCause
MISSING_ATTRIBUTEIn 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_ERRORHandshake failed.
SOCKET_RECONNECT_FAILED10 reconnect attempts exhausted.
API_LOAD_FAILEDThe 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_DROPPEDA send was attempted while the socket was down.
CONVERSATION_CREATED_REFETCH_FAILEDRefetch after a new conversation failed.
GROUP_JOIN_REFETCH_FAILEDRefetch 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 emit gc:tail-bet, so a window-level listener receives events from either.
  • goma-my-bets: embedded inside the share-a-bet screen and configured through the myBets property. 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-bet carries the selections a viewer wants to copy; the host writes them into the goma:betslip:v4 envelope, 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

  1. Opened the dock
    gc:open. The earliest engagement signal; gc:connected fires earlier but is not user-initiated
  2. Had something waiting
    gc:unread-count with a non-zero count
  3. Received a message
    gc:message-received. From someone else only
  4. Replied (good)
    gc:message-sent. Optimistic, see the traps below
  5. Grew their network (caution)
    gc:friend-added. Intent, not outcome
  6. 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.

StateWhat the viewer sees
No tokenNothing. The widget is mounted but renders no bubble
Token present, connectingThe bubble, with the panel closed
ConnectedBubble plus, on open, the conversation lobby
Token clearedSocket 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:

MissingWhat you get
socket-url, with api-url presentgc:error with code: 'MISSING_ATTRIBUTE' and an attributes array: the one observable case
api-urlNothing. The validation that would report it sits inside a bootstrap that never runs
auth-tokenNothing. 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 button

Older 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:

ElementSizePosition
Bubble56 × 56bottom: 20px; right: 20px: override with --goma-messengerFabBottom / --goma-messengerFabRight
Panel380 × 640, max-height: 85vhbottom: 80px; right: 20px
Panel, at 767px and belowFull-screenFills 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:

TokenDefault
--goma-messengerFabBottom20px
--goma-messengerFabRight20px

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:

PresetStyle
exclusivebetThemeExclusivebet brand colours: dark charcoal with a gold accent
betssonfranceThemeBetsson France brand colours
betathomeThemebet-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.

TokenDefaultUsed for
--goma-backgroundPrimary#091737Chat panel background
--goma-backgroundSecondary#0C2251Header / footer background
--goma-backgroundTertiary#0E2B65Secondary panel background
--goma-backgroundCards#0E2B65Card background
--goma-chatBorderElevated#11367FElevated surface border
--goma-chatBubbleMine#533BFFOwn message bubble
--goma-chatBubbleOther#0C2251Other message bubble
--goma-highlightPrimary#C0CE00Primary accent: bubble, CTAs, friend code
--goma-highlightSecondary#533BFFSecondary accent
--goma-chatHighlightSecondary#533BFFSecondary accent, chat surfaces
--goma-highlightTertiary#83FF4ATertiary highlight
--goma-alertSuccess#2BDA3FSuccess states
--goma-alertError#FF6767Error states, unread badge
--goma-alertWarning#f59e0bWarning states
--goma-textPrimary#FFFFFFPrimary text
--goma-textSecondary#ADE4FFSecondary text
--goma-allDark#091737Text on accent surfaces
--goma-chatBubbleTextMine#FFFFFFOwn bubble text
--goma-chatBubbleTextOther#FFFFFFOther bubble text
--goma-inputTextTitle#ADE4FFMuted text: labels, hints, placeholders
--goma-chatTimestamp#ADE4FFMessage timestamps
--goma-textDisablePrimaryrgba(255,255,255,0.28)Section headers, disabled text
--goma-buttonBackgroundPrimary#C0CE00Primary button
--goma-buttonTextPrimary#091737Primary button text
--goma-buttonActiveHoverPrimary#a8b400Primary button, hover / active
--goma-buttonDisablePrimaryrgba(192,206,0,0.4)Primary button, disabled
--goma-buttonBackgroundSecondary#533BFFSecondary button
--goma-buttonTextSecondary#FFFFFFSecondary button text
--goma-buttonTextTertiaryrgba(255,255,255,0.48)Ghost button text
--goma-buttonBorderTertiaryrgba(255,255,255,0.12)Ghost button border
--goma-inputBackgroundSecondary#0E2B65Input background
--goma-inputBorderActive#C0CE00Focused input border
--goma-inputText#FFFFFFInput text
--goma-inputPlaceholder#FFFFFFInput placeholder
--goma-iconPrimary#FFFFFFPrimary icons
--goma-iconSecondaryrgba(255,255,255,0.48)Secondary icons
--goma-chatIconGroup#ADE4FFGroup avatars
--goma-separatorLine#143F93Borders
--goma-separatorLineSecondary#143F93Separators
--goma-radiusPill22pxPill radius: the only shape token in this set
--goma-chatBetLegBackground#0C2251Bet-card leg background
--goma-chatBetScore#C0CE00Bet-card leg score
--goma-liveTag#C0CE00LIVE chip background
--goma-liveTagText#091737LIVE chip text
--goma-myTicketsWon#0F5531Won status
--goma-myTicketsLost#60111BLost status
--goma-myTicketsOther#946F00Other / partial status
--goma-scrollrgba(255,255,255,0.08)Scrollbar thumb
--goma-fontFamily'Exo 2', system-ui, -apple-system, sans-serifTypeface
--goma-fontSize2xs9pxFont size: 2xs
--goma-fontSizeXs10pxFont size: xs
--goma-fontSizeSm12pxFont size: sm
--goma-fontSize14pxFont size: base
--goma-fontSizeLg16pxFont size: lg
--goma-messengerFabBottom20pxBubble offset from the bottom
--goma-messengerFabRight20pxBubble offset from the right
--goma-chatShadowSm0 1px 2px rgba(0,0,0,0.2)Small shadow
--goma-chatShadowMd0 4px 6px -1px rgba(0,0,0,0.3)Panel shadow
--goma-transitionNormal250ms easeTransition duration

One more token is read but not declared in variables.css, so it has no stylesheet default:

TokenDefaultUsed for
--goma-secondaryFloaternone (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 no Upgrade header would be mangled. Map the header so it echoes upgrade only when the client asked for one, and raise proxy_read_timeout well 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 answers index.html with a 200. The client's JSON.parse then 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 createWidgetElement factory (it declares its own HTMLElement subclass 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, await window.__gomaCdn.ready first, 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. Set cashoutEnabled (attribute cashout-enabled) to false to turn it off. The widget emits two new events: goma:cashout-success (with betId, cashoutType, cashoutAmount, pending and, for a partial cash out, partialCashoutStake) and goma:cashout-error (with betId, cashoutType, a reason of not-signed-in, invalid-stake, network, http-error, rejected or unknown, and, when known, the HTTP status and the provider's message). Three new image properties, myBetsCashoutWarningImage, myBetsCashoutSuccessImage and myBetsCashoutErrorImage, 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. cashoutAmount is 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, pending is true, the bet stays in the Open list and the list is fetched again.

    <goma-betslip-sidebar> and <goma-betslip-floating> forward the new myBetsCashoutEnabled attribute (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: openSse accepts a method option and a body option, so a stream can be opened with a POST request. openSse also calls onClose when its retries run out after an error, so a consumer knows the stream is finished.

    @gomagaming/my-bets holds 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, and goma:my-bet-select can now report tab: '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-domain has 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, useMyBetsCashoutValues and useMyBetsCashout, and a new requestBalanceRefresh action on the user store.

    @gomagaming/core and @gomagaming/ui carry the shared dialog and slider styles. GomaDialog in @gomagaming/ui takes an optional panelClass with extra classes for the panel; when set, it also replaces the default border colour. In @gomagaming/core, useScrollLock now 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 new subTab property (attribute sub-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 new myBetsSubTab property (attribute my-bets-sub-tab) that sets the sub-tab inside the My Bets tab.

    cashoutEnabled on <goma-my-bets> and myBetsCashoutEnabled on 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 sends WITHIN_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: the goma:place-bet-request detail now carries oddsValidationType: '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 accept WITHIN_THRESHOLD. The custom-elements.json type 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 freeBet as a boolean: true only when the ubsWalletId on that payload belongs to a free-bet wallet (so the provider books a free bet and not a cash stake), false otherwise, where it sent null before; the goma:place-bet-request detail type changes from null|object to boolean. A betslip pick now sets ubsWalletId and freeBet together and wins over a host placeBetBody.freeBet; with no betslip pick a host that sends a free-bet wallet through placeBetBody.ubsWalletId must also send freeBet: true. The picked wallet's bonus type is now stored with the pick (and shared like the wallet id) as { walletId, type } in selectedBonusType and selectedBonusTypeBySelection of the goma:betslip:v4 envelope, 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, ubsWalletId is now absent instead of null when no bonus is picked, and oddsBoostPercent and capAmount are 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. The possibleWinnings of goma:bet-placed keeps 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-request and in the place-bet body, freeBet is now a boolean, and a betslip pick sets it. A host that used to put a free-bet token object in placeBetBody.freeBet must stop doing that. Send freeBet: true only when you send your own free-bet wallet id and the betslip has no pick.

    @gomagaming/sports-domain has two new exports: isFreeBetWallet (whether a wallet id belongs to a free-bet wallet) and freeBetNetWinnings (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 / quickAddAmountsJson are now placeBetHeaders / 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 highlightedImages string default was indexed character by character and requested bad image URLs on every load. The missions-hub emptyStateImages illustrations now show.

    No widget runtime behaviour changes.

0.36.0
  • Add a bubble-variant attribute ('primary' | 'secondary', default 'primary') so a host can pick which floater the chat bubble uses. secondary is 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-variant attribute and the bubbleVariant property (same state), and as a knob in the playground. The docs page lists it next to position and 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 through toLocaleDateString. With locale: 'en' (the bare tag, which Intl resolves to American ordering) that long form came out as October 11 next to the card's 11/10. English was the only shipped locale that disagreed with its own cards (fr and pt are already day-first, and the shared time formatter has always been pinned to en-GB), so the long form now resolves a bare en to en-GB. A host that explicitly configures en-US keeps American ordering.

    Two surfaces build their own Intl.DateTimeFormat rather 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 printed 07 October where the cards print 7 October, and sat directly under a hand-built dd/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/YY two 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) named Inter. That face is not one the widget ships: its fontUrl loads 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-fontFamily saw 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 (alongside font-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-fontFamily to 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's createWidgetElement() factory, and live-chat's and messenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): the HTMLElement subclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw a ReferenceError immediately.

    Per-package fixes:

    • favorites: added src/themeTokens.js and a :host stylesheet (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's dataSource away from rpc/subscribe never released the open topic.
    • filters-settings: fixed a dead :host{display:inline-block} CSS rule (the shared element factory's inline display:block always won), so the widget always rendered full-width instead of the intended compact inline trigger.
    • game-details: added a min-height fallback for the inert/no-eventId state 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. Corrected themeTokens.js registries that had drifted from actual token usage, added missing goma:retry Events-table rows, and other small doc/registry corrections.
    • region-filter: added a missing typeof window !== 'undefined' guard on a window listener.
    • live-chat: corrected custom-elements.json's members array (missing theme/config accessors, 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 in docs/guide/integration-caveats.md as 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 honour prefers-reduced-motion. Neither package emits a dist/style.css any 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 to color: rather than font-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-url stylesheet was injected into the shadow root, and Firefox ignores @font-face declared there. The widget silently fell back to a system font. It now goes to document.head, deduped and shared across instances, matching what messenger already did.

    live-chat: logging out closes the socket. Clearing auth-token emptied 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/json with a zero-length body, obliging every backend to special-case an empty payload it had been told to parse.

    live-chat: betathomeTheme is 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 returning null for 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 per applyTheme() 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 register goma-live-chat and goma-messenger alongside 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/livechat has 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/messenger no 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/core and @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 the theme attribute, the .theme / .config.theme properties, 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 flat components/ / composables/ / stores/ / utils/ / api/ shape every other widget uses. The public surface (the <goma-messenger> tag, its attributes, the config / theme / myBets properties, and every gc:* event) is unchanged. Only a host reaching past the package entry point into one of the moved internal paths is affected.

    @gomagaming/live-chat gains a .theme property accessor alongside its existing .config: reading it returns the --goma-* custom properties the widget actually painted (not a mirror of config.theme); assigning routes through applyTheme(), with the same key-normalisation and retired---gc-* warning described above. No attribute or event changed.