Skip to content

<goma-my-bets> ​

Standalone My Bets widget: renders the signed-in user's bets across four sub-tabs (Open / Closed / Won / Cash Out) with live match scores ticking on open bets and cash out (full or partial) on open bets that the provider quotes a value for. Both betslip widgets (goma-betslip-sidebar and goma-betslip-floating) embed this element for their own My Bets tab rather than implementing bet history themselves, so this page is the single reference for the bet-history feed whether you're using it standalone (an account area, a dedicated "My Bets" route) or reading it to understand what's happening inside a betslip's My Bets tab.

The two chat widgets embed it as well, in their share-a-bet screens, and goma-live-chat additionally requires this package to be installed even when bet sharing is switched off. Both configure it through their own myBets property rather than a config object; see goma-live-chat and goma-messenger.

Live demo ​

Open in the playground →: toggle My Bets in the widget rail and feed a sessionId in the config panel to see the authenticated feed load.

Element tag ​

html
<goma-my-bets></goma-my-bets>

The widget sets :host { display: block; width: 100%; height: 100% } and falls back to min-height: 360px when the host gives it no definite height: give it a sized container (definite height or a flex column) for best results.

Installation ​

@gomagaming/my-bets is a scoped private package. See Installation for the one-time .npmrc setup required to consume it.

bash
pnpm add @gomagaming/my-bets

You also need the shared peer dependencies installed at your app level:

bash
pnpm add vue pinia vue-router vue-i18n @vueuse/core autobahn-browser

vue, pinia, vue-router, vue-i18n, @vueuse/core, and autobahn-browser are declared as peerDependencies so a consumer with multiple @gomagaming/* widgets gets exactly one copy of each. autobahn-browser is only exercised when config.socketUrl is set (live open-bet scores): the widget works over plain REST without it.

Configuration ​

js
const el = document.getElementById('mb')
el.config = {
  betsApiBaseUrl:   'https://bets-api.example.com',   // bets-api feed (falls back to apiBaseUrl)
  playerApiBaseUrl: 'https://player-api.example.com',  // session probe (falls back to apiBaseUrl)
  ucsOperatorId: '2838',
  socketUrl: 'wss://sports-ws.example.com',            // optional: enables live open-bet scores
  socketRealm: 'sportsbook',
  provider: 'everymatrix',
  locale: 'en',
  theme: { /* --goma-* overrides */ },
  messages: { en: { /* i18n overrides */ } },
}
KeyFallback chainPurpose
betsApiBaseUrl→ bettingApiBaseUrl → apiBaseUrlBase URL for the bets-api feed (open-bets / settled-bets). It also serves the cash out value stream and the cash out request.
playerApiBaseUrl→ apiBaseUrlBase URL for the session probe (/v1/player/session/player).
ucsOperatorId-Operator id: path segment + X-operator-id header on bets-api calls; also the WAMP topic segment for live scores.
socketUrl / socketRealm-Optional WAMP connection for the match scores (live on open bets, final on settled bets with no result), the live state behind the LIVE pill, and the match clock. Omit and the widget still loads bets over REST: see Empty / loading / error states.
provider-Backend adapter selector (mirrors the other widgets' config.provider).
locale-Propagates to X-language on bets-api requests and to the bundled i18n.
theme-CSS variable overrides, same shape as every other widget.
messages-i18n message overrides, same shape as every other widget.

CORS is the host's responsibility: the widget makes vanilla cross-origin fetch calls; ensure the relevant Access-Control-Allow-* headers are in place (or front the requests with a same-origin reverse proxy).

Data sources ​

There is one feed path: an authenticated REST feed, gated by a session probe, with an optional live-score overlay. Cash out adds a value stream and a request on the same bets-api host (items 4 and 5).

  1. Session probe: setting sessionId (with config.playerApiBaseUrl configured) triggers GET {playerApiBaseUrl}/v1/player/session/player with header X-SessionId. A response shaped { IsAuthenticated: true, UserID } populates the per-instance user store (userId, isAuthenticated); any failure, non-200, or IsAuthenticated: false clears the session and the widget falls back to the guest empty state.

  2. Bets feed: once authenticated (and ucsOperatorId + betsApiBaseUrl are resolvable), the widget fetches the active tab:

    • Open → GET {betsApiBaseUrl}/bets-api/v1/{ucsOperatorId}/open-bets
    • Closed → GET {betsApiBaseUrl}/bets-api/v1/{ucsOperatorId}/settled-bets
    • Won → GET {betsApiBaseUrl}/bets-api/v1/{ucsOperatorId}/settled-bets?betStatus=WON

    Requests carry X-language, X-session-id, X-operator-id, X-user-id headers and cursor-paginate via limit=20 + placedBefore=<ISO timestamp>; scrolling past ~70% of the list fetches the next page (a short page marks the tab exhausted). Switching tabs resets the scroll position and re-fetches from now(). The fourth sub-tab, Cash Out (cashout → settled-bets?betStatus=CASHED_OUT), lists bets that were fully cashed out. It shows only while cashoutEnabled is on (see Cash out).

  3. Live open-bet scores: when config.socketUrl is set, the widget opens a per-event eventPartScores/small WAMP feed for every open, unsettled selection that is live or past its kick-off time, and writes the ticking score in place onto that leg. A leg that has not kicked off yet is checked again at its kick-off time, so a game that starts while the list stays open gets its feeds with no reload. The same legs also get a per-event status feed (match-aggregator-groups-overview/{eventId}/1: the match record and its event infos, no markets). It decides whether the game is in play right now, which swaps the leg's kick-off date for the LIVE pill, and it gives the match clock that showLiveClock puts in front of the pill. A finished match drops out of that feed, so the pill goes away when the game ends, even while the bet waits to settle; the score feed keeps the final score. Until the status feed answers, and with no socket at all, the bets-api isLive flag decides the pill. Placed odds are never touched or re-fetched: only the match score updates live. Closed/Won bets never receive live updates (their outcome is already settled). A settled selection shows the final score the bets-api returns in its eventResult; when that field is empty, the widget asks the same eventPartScores/small topic once for the final score and shows it on the leg. Without socketUrl, the widget still mounts, the session probe and bets-api feed still run over REST, and all tabs render normally. What is disabled: the live-ticking score on open bets, which falls back to the frozen fixture score captured at place-bet time; the one-time final score for settled legs; and the match clock. The LIVE pill then follows the bets-api isLive flag alone. There is no error state or degraded UI for the missing socket; it's a silent, expected configuration.

  4. Cash out values (stream): while cashoutEnabled is on, the player is signed in, the Open tab is active and the open list is not empty, the widget opens one server-sent stream: POST {betsApiBaseUrl}/bets-api/v1/{ucsOperatorId}/cashout-value-updates. The body is { "betIds": [...] } with the ids of all open bets. The headers are the same as for the feed, plus a userId header. Each message carries a bet id and its current cash out value; the card shows a new value as soon as it arrives. A value of 0, null or a non-number hides the cash out control for that bet. The stream needs no WAMP socket. It runs only while the widget is rendered (a widget that is hidden or has zero size opens no stream) and only on the Open tab. The widget closes it when the player leaves the Open tab, signs out or turns cashoutEnabled off, and opens a new one when the set of open bets, the session or the language changes. If the stream drops, the widget tries again up to 5 times and keeps the last known values while it does. When the tries run out, the values are cleared, so no card keeps an old price. The widget opens the stream again when the browser goes back online or when the tab becomes visible again.

  5. Cash out request: confirming the dialog sends POST {betsApiBaseUrl}/bets-api/v1/{ucsOperatorId}/cashout with the same headers. The body has betId, cashoutValue, cashoutType ('FULL' or 'PARTIAL'), cashoutChangeAcceptance: 'WITHIN_THRESHOLD', operatorId and language. A 'PARTIAL' request also has partialCashoutStake. The request counts as failed when the call throws (network error or a non-2xx status), when the body is empty or not an object, or when the body does not have success: true. A missing success flag is a failure. The paid amount is the first number found in cashoutPayout, cashoutValue or cashoutAmount of the answer, or the quoted value when the answer has none. When the answer has pendingCashOut: true, the provider accepted the cash out but holds it for approval. A 401 or 403 here shows the error step only. It does not end the session. After a full cash out the bet leaves the Open list, unless the cash out is pending. A pending cash out (full or partial) keeps the bet and fetches the Open list again, so the list shows the server state. After a partial cash out the Open list is fetched again, and the stream is opened again with the new stake.

Props / attributes ​

PropertyAttributeDefaultDescription
sessionIdsession-id''Authenticated-session token. Validated against config.playerApiBaseUrl to derive userId + isAuthenticated; once resolved the bets-api feed loads. Empty string renders the logged-out empty state.
ucsOperatorIducs-operator-id''Operator id. Can also be set via config.ucsOperatorId; either surface works.
myBetsEmptyGuestImagemy-bets-empty-guest-image''URL of the guest empty-state illustration: shown with "Offside! / Log in to view your bets." when there's no sessionId. Omit for a flag-emoji fallback.
myBetsEmptyAuthedImagemy-bets-empty-authed-image''URL of the signed-in-no-bets empty-state illustration: shown with "Empty stands… / Place a bet and light up the game." Omit for a stadium-emoji fallback.
iconBaseUrlicon-base-url''Standard goma property (inherited from createWidgetElement's common defaults, not declared by this widget specifically). Used as the <img> fallback base for a leg's sport-icon glyph ({iconBaseUrl}/icons/sports/dark_mode/{sportId}.svg) for any sport id outside the bundled SVG set; bundled sports render inline regardless.
flagBaseUrlflag-base-url''Declared on this widget's public surface, but not currently wired to anything: the bet-card leg flag glyph always resolves through getFlagIconViaCdn(), which reads a module-level default CDN base that nothing in the codebase ever overrides via its setFlagCdnBase() escape hatch. Setting this prop has no observable effect on the flag glyph today.
reloadTokenreload-token0Bump to force a refetch of the active tab: equivalent to calling reload(). Useful when a host places a bet through its own flow (outside <goma-betslip-*>) and wants My Bets to refresh.
cardSelectablecard-selectabletrueGates the entire bet-card tap/keyboard affordance (role="button", tabindex, cursor, focus ring, click/Enter/Space) plus the goma:my-bet-select emission. Defaults on, and stays on even when this element is mounted inside a betslip's My Bets tab (see Integrates with): set to false to make cards inert.
cardRadiocard-radiofalseOpt-in per-card radio selector: an empty circle top-right of each card's header, filled with --goma-highlightPrimary (inner dot --goma-highlightPrimaryContrast) for the single currently-selected card. Purely visual and additive: it never gates the tap affordance or the goma:my-bet-select emission, only which card renders filled; because the tap handler itself only attaches when cardSelectable is true, a card stays inert (and its radio can never fill) if cardSelectable is false. Selection state is local to the panel instance and resets on remount.
showLiveClockshow-live-clockfalseShows the match clock (the match minute and the current part, e.g. 33' 1st Half) in front of the LIVE pill on a live leg. The LIVE pill itself shows on every live leg either way. Needs config.socketUrl: the clock comes from the per-event status feed, see Data sources.
cashoutEnabledcashout-enabledtrueOffers cash out, full and partial, on open bets that the provider quotes a value for. Any value except false turns it on. Set to false to start no cash out stream and render no cash out dialog. The property also takes the string 'false' as off.
subTabsub-tab'open'The sub-tab the widget opens on, and switches to when the value changes: 'open', 'settled', 'won' or 'cashout' (the same values as the tab field of goma:my-bet-select). 'closed' also works and means 'settled'. The player can still click any tab. Setting the same value again moves the widget back to that tab, also after the player clicked another one (the property does this; the attribute only acts when its value changes). 'cashout' shows Open while cashoutEnabled is off. An unknown value logs one warning and keeps the current tab. A guest (no sessionId) also opens on subTab, and so does the widget after a sign-out and at the next sign-in. The first request after a sign-in is for that tab.
myBetsCashoutWarningImagemy-bets-cashout-warning-image''URL of the illustration on the cash out confirm step. Nothing renders when unset.
myBetsCashoutSuccessImagemy-bets-cashout-success-image''URL of the illustration on the cash out success step. Nothing renders when unset.
myBetsCashoutErrorImagemy-bets-cashout-error-image''URL of the illustration on the cash out error step. Nothing renders when unset.

The widget also inherits the standard goma surface from createWidgetElement: locale, theme, messages, debug, config.

Events ​

StatusEventDetailEmitted when
Canonicalready{}Widget mounted, initial render done.
Canonicalgoma:my-bet-select{ bet, tab }User tapped a bet card. bet is the full mapped Bet object (betId, status, selections, amount, currency, totalPriceValue, …); tab is the active sub-tab: 'open' | 'settled' | 'won' | 'cashout' ('settled' is the Closed tab, 'cashout' is the Cash Out tab). Only fires when cardSelectable is true (the default). Fires identically whether or not cardRadio is enabled: the radio fill is a side effect of the same tap (it updates which card's betId is tracked as selected), not a separate emission.
Canonicalgoma:cashout-success{ betId, cashoutType, cashoutAmount, pending, partialCashoutStake? }A cash out request succeeded. cashoutType is 'FULL' or 'PARTIAL'; cashoutAmount is the amount the provider paid (its cashoutPayout), or the quoted value when the provider sends none; pending is true when the provider accepted the cash out but holds it for approval, and false otherwise; partialCashoutStake is the part of the stake that was cashed out and is present only for a 'PARTIAL' cash out. Fires once per request, also when the player closed the dialog while the request was running.
Canonicalgoma:cashout-error{ betId, cashoutType, reason, status?, message? }A cash out request failed or was rejected. The bet is unchanged. reason says why (see Error reason). status is set for http-error only. message is the provider's own text, when it sent one.
Canonicalgoma:session-ready{ userId, username }The session finished resolving into an identity that can actually place a bet: the CE-login exchange completed and the payload now carries the provider-side username. This widget does not run the CE-login exchange itself: only useSession()'s REST probe, so this event only fires when it observes a shared session envelope another session-aware widget on the same page (a betslip, or <goma-favorites>) has already resolved via its own CE-login exchange. A standalone <goma-my-bets> with no such sibling still loads bets fine over REST, but never emits this event. Fires once per resolved token per mounted session-aware widget, and again whenever a new token is minted (max-age refresh, WAMP reconnect): handlers must be idempotent.
Canonicalgoma:session-expired{ reason, invalidationId }The player's session is no longer usable and only the host can fix it: GomaPulse cannot mint a replacement token. Fires when the provider rejects the credential (this widget's own session probe, or a 401/403 from any other session-aware widget on the page). reason is one of session-probe-401, session-probe-403, session-probe-rejected, plus reasons only other widgets can produce (place-bet-401, place-bet-403, favorites-401, favorites-403, ce-login-rejected) surfaced here because the envelope is shared. Host listens to flush its own session state and re-authenticate by setting a fresh sessionId. Fires once per invalidation per mounted session-aware widget, and again on a retry: dedupe on detail.invalidationId, which is identical across widgets for one death and higher on a retry.
Canonicalgoma:error{ message, code, component? }Two paths. Connection layer: the WAMP adapter (config.socketUrl, used for live open-bet scores) exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry.
Canonicalgoma:retry{}User clicked the retry button on the error boundary banner.

All events bubble + composed, so they cross the Shadow DOM boundary and are observable from window.addEventListener(…).

Methods ​

js
el.reload()

Imperatively refetches the active sub-tab (bumps reloadToken internally). Equivalent to setting el.reloadToken = el.reloadToken + 1 or bumping the reload-token attribute: use whichever is more convenient for your host framework.

Integrates with ​

No follow* prop: this widget doesn't listen for cross-widget selection events. Its two cash out events (goma:cashout-success, goma:cashout-error) are for the host and for the betslips (see below). It participates in the shared session lifecycle: it runs its own useSession() REST probe against sessionId, mirrors the shared goma:session:v1 envelope (see goma:session-ready / goma:session-expired above).

useMyBets() also watches the betslip store's lastPlacedBet field and force-reloads the active tab the instant it changes: the same store write that also emits goma:bet-placed. This only fires when <goma-my-bets> is embedded inside a betslip's My Bets tab in hosted mode (see below): hosted mode shares the parent's actual Pinia instance, so the two widgets read and write the same store. A standalone <goma-my-bets> gets its own isolated Pinia (per the framework's one-createPinia()-per-instance rule) and never sees a sibling betslip's placements this way: use reload() / reloadToken there instead. The betslip does this itself for an embedded <goma-my-bets> that mounted standalone (see below).

<goma-betslip-sidebar> and <goma-betslip-floating> embed this element directly for their My Bets tab (a bundled dependency, not a peer pairing): BetslipPanel.vue mounts <goma-my-bets> and feeds it a subset of state (config, theme, locale, sessionId, myBetsEmptyGuestImage, myBetsEmptyAuthedImage, and myBetsShowLiveClock as this widget's showLiveClock, myBetsCashoutEnabled as its cashoutEnabled, myBetsSubTab as its subTab, and the three cash out images) whenever the session changes, and re-sends theme, locale, myBetsShowLiveClock, myBetsCashoutEnabled and myBetsSubTab when they change at runtime. The three cash out images are also re-sent when they change, but only when the new URL is not empty. It keeps the element mounted (v-show) so it boots its Vue tree once. The betslip also listens for goma:cashout-success from its own My Bets tab: it asks for a new balance at once and again after 2.5 seconds, because the wallet can need a short time to show the payout. A standalone host must refresh its own wallet display on that event. <goma-live-chat> and <goma-messenger> embed this element in their share screens with cash out turned off. In that embedded position <goma-my-bets> normally mounts in hosted mode (see Theming and Empty / loading / error states below for what that changes). In hosted mode the live open-bet scores ride the betslip's own socket connection: set socketUrl on the betslip's config and the My Bets tab ticks live scores exactly as the standalone widget does.

When the page loads two copies of @gomagaming/core (for example the betslip from the CDN and another widget that bundles @gomagaming/my-bets from npm), the embedded <goma-my-bets> can come from the other copy. It then mounts standalone inside the betslip, because it cannot share a Vue app and Pinia with a different copy:

  • It has its own Shadow DOM, Pinia and i18n. The betslip forwards locale so a language switch still reaches it, and calls reload() after each placement, because the shared lastPlacedBet store is not visible to it.
  • It runs its own session probe and, with config.socketUrl, its own socket connection for live scores.
  • Its lifecycle events (ready, goma:connection, goma:connected, goma:session-ready, goma:session-expired, and goma:error with code API_INIT_ERROR) stay inside the betslip's shadow root, so the page sees each of them once, from the betslip. Its other events (for example goma:my-bet-select) reach the page as they do in hosted mode.
  • It adopts its own :host stylesheet, so its token defaults apply (see the hosted-mode note under Theming).

See Cross-widget wiring and Shared state for the full session-envelope contract.

Bonus on a bet card ​

A bet placed with a bonus shows it on its card. The data comes from the bet feed, so nothing needs configuring. A bet with no bonus is unaffected.

Stakeback ​

TopicDetail
What showsA tag with the rate the bonus pays back, for example 5% Stakeback
WhenIn every settlement state. The bonus belongs to the bet, not to its outcome, so the player sees it while the bet is still running.
Several tiersThe highest tier is shown
Feed fieldstakeBackOptions.sportsStairs
Copymy_bets_stakeback_tag ({percentage} Stakeback). You can override it like any other message, see i18n.
Refund rowOn a lost or half-lost ticket, a "Stakeback Bonus" row with the refund. Its info control explains the figure: "{refund} ({percentage} of your {stake} bet amount) was delivered to your account balance." The rate is only named when the ticket carries exactly one tier AND the refund matches stake x rate; a ticket with no tier, an empty tier list, more than one tier, a tier with no usable rate, or a refund below stake x rate (for example a capped refund) falls back to "{refund} was delivered to your account balance."
Refund sourcebetStakeBack on the feed (a number, or a map keyed by currency). No refund is computed on the device: if the feed sends none, there is no row.

Odds boost ​

While the bet is open:

  • the footer's winnings figure shows the pre-boost amount with a strikethrough, because it is no longer what the player stands to win,
  • a "WIN BOOST (x%)" row under the footer shows the boosted figure.

Once the bet settles, the winnings figure shows the settlement, with no strikethrough:

StatusWinnings figureBoost row
WONThe payoutShown, with the same payout
LOST0.00Hidden
CASHED_OUTThe cashout amountHidden: the boosted figure is money the player never received

Feed fields:

  • oddsBoost: a boolean on this feed (not the boosted total that the place-bet response carries)
  • oddsBoostPercentage
  • amount and totalPriceValue (the combined odds). The feed does not send the pre-boost winnings, so the widget derives it as amount × totalPriceValue.
  • maxWinning: the boosted figure

Copy comes from win_boost_format (WIN BOOST ({percentage}%)).

Free bet ​

A bet placed with a free-bet wallet (freeBet: true on the feed) pays the winnings only, because the stake was the free bet and is not returned. The provider's free-bet tickets include the stake in maxWinning and currentPossibleWinning, so the card never shows those figures. The winnings figure is:

StatusWinnings figure
OpenpotentialNetReturns; when the feed has none, max(0, amount × (totalPriceValue − 1))
WON, HALF_WON, VOID, CANCELLED, DRAW, PUSHEDthe open-bet figure above. betPayout is not read for a free bet: whether the provider adds the free stake to it, as it does to maxWinning, is not verified
LOST, HALF_LOST0.00
CASHED_OUTThe cashout amount, as for any bet

Known gap. A settled free bet that is HALF_WON, VOID, CANCELLED, DRAW or PUSHED shows the net potential, which is not what the provider paid for it. The provider's paid figure for a settled free bet (betPayout, and whether it includes the free stake) is not confirmed, so the card keeps the net figure until it is.

A bet without the free-bet flag is unaffected. An open cash bet reads currentPossibleWinning, then maxWinning. A won cash bet reads betPayout first, then maxWinning. A free-bet ticket never offers cash out.

Feed fields: freeBet, potentialNetReturns, amount, totalPriceValue.

Cash out ​

Cash out lets the player take a payout for an open bet before the events end. It is on by default (cashoutEnabled). Set cashoutEnabled to false and the widget opens no stream and renders no cash out control or dialog.

When a bet offers cash out ​

A card shows the cash out row only when all of these are true:

  • The bet is open (running) and has a bet id.
  • The bet is not a free bet. A free-bet ticket never offers cash out.
  • The provider has not marked cash out as disabled for this bet (isCashoutDisabled).
  • The provider sent a current value for this bet, and the value is greater than 0.

Settled bets (Closed and Won tabs) never show it, and neither does a bet that the provider did not quote. The row sits at the bottom of the card. It has a Cash Out button with the live value. When the provider allows a partial cash out and the value is above the minimum, a second, outlined button with a slider icon opens the partial dialog. A tap on these buttons does not select the card.

Steps in the dialog ​

The dialog is a native <dialog>, centred on every screen size: up to 480px wide, with a 24px margin on each side on a phone. It keeps the value the player saw when the dialog opened. Later price changes do not change the open dialog. The dialog closes by itself when the bet can no longer be cashed out (the value is gone or disabled, for example when the bet settles, the stream ends or the player changes sub-tab). It does not close on the success step or while a request runs.

StepWhat the player sees
Slider (partial only)A ? button on the left and a Close link on the top right. Then a title, an amount field and a slider to set the amount, with the minimum and maximum, and the remaining bet amount. The ? button shows a short explanation in a tooltip above it, on hover, on keyboard focus or on a tap. The amount field shows the amount with its currency symbol and accepts a dot or a comma as the decimal mark. The amount is limited to the range from the minimum to the full value. A typed amount is rounded down to whole cents. A choice at the full value is a full cash out. A choice that rounds to a stake of 0 cannot continue.
ConfirmThe question, a short text and the warning picture, with the Cancel and Confirm buttons. On a partial cash out the text names the amount to be paid and the stake that stays in play, and Cancel goes back to the slider.
SuccessThe success picture, the total odds, the remaining stake (0 after a full cash out) and the paid amount. The dialog closes by itself after 2.5 seconds, or at once on Done.
ErrorThe error picture and a message that the bet is still active, with Return and Try again. Try again shows the current cash out value of the bet again, before the player confirms. A partial cash out returns to the slider, with the chosen amount kept inside the new range. It sends no request by itself. If the bet can no longer be cashed out, the dialog closes.

Rules ​

  • A partial cash out pays the chosen amount and cashes out the matching part of the stake: stake times (chosen amount divided by full value), rounded to two decimals.
  • After a partial cash out the card shows a Cashed Out tag next to the title. A box under the buttons shows Partial Cashed Out with the amount already paid out and Remaining Amount with the stake still in play (betRemainingStake). The Bet Amount keeps the stake as placed, and the stream is opened again with the new stake.
  • A fully cashed-out bet keeps the neutral card look (no coloured strip or footer) and shows no result on its legs. A box at the bottom shows Cashed Out with the total paid out (overallCashoutAmount, else amountCashOut).
  • Possible Winnings is always the provider's currentPossibleWinning (else maxWinning). The widget does no maths on it, except for a free bet, where it shows the winnings without the stake.
  • After a full cash out the bet leaves the Open list, unless the provider holds the cash out as pending. A pending bet stays in the list, and its cash out price is removed until the list is fetched again.
  • If the player closes the dialog while the request runs, the request still finishes, and the matching event is still emitted.
  • If the session ends or cashoutEnabled turns false, an open dialog closes.
  • The widget emits goma:cashout-success or goma:cashout-error once for each request. See Events.

Error reason ​

goma:cashout-error carries a reason. The error step looks the same for every reason; the host can use reason for its own message or tracking.

reasonWhen
not-signed-inThe widget has no session, user, operator id or bets-api base.
invalid-stakeA partial cash out with no usable stake.
networkThe request failed with no answer (offline, blocked).
http-errorThe provider answered with a non-2xx status. status holds the number.
rejectedThe provider answered 2xx but not with success: true.
unknownAny other failure.

message is set only when the provider sent a text in its answer (message, else errorMessage, else error as a string). It is never our own error text.

Tab after a cash out ​

When the success step closes, the widget shows the tab where the bet now is. This holds for every way out: Done, the close button, the backdrop, Escape and the 2.5 second timer. A full cash out that is not pending moves to the Cash Out tab. A partial cash out, and any pending cash out, show the Open tab, because the bet is still unsettled. If the widget is already on that tab, it does not switch. The tab never changes while the success step is on screen. This routing wins over subTab at that moment; subTab is applied again the next time the host sets it or the player signs in.

The Cash Out tab ​

The fourth sub-tab, Cash Out, lists the bets that were fully cashed out. Two rules apply:

  • A bet that was only partly cashed out is still unsettled. It shows in Open only, with its remaining stake, and never in the Cash Out tab.
  • The Cash Out tab shows only bets with the status CASHED_OUT. The widget also removes a CASHED_OUT bet from the Open list, in case the provider returns one there.

The tab shows only when cashoutEnabled is on. If cashoutEnabled is turned off while the Cash Out tab is active, the widget moves to Open. After a full cash out that is not pending, the widget clears its saved Cash Out list. The provider can take a few seconds to list the bet after a full cash out, so a player who opens the Cash Out tab at once may not see it yet. The next visit to the tab fetches the list again.

The three illustration props (myBetsCashoutWarningImage, myBetsCashoutSuccessImage, myBetsCashoutErrorImage) are URLs. When one is empty, that step shows no picture.

Empty / loading / error states ​

  • Logged-out. No sessionId (or it clears): guest empty state ("Offside! / Log in to view your bets.").
  • Loading (first page). Session resolving or the first bets-api fetch in flight for a tab with no cached bets yet: centred spinner, role="status" + aria-busy="true".
  • Refreshing (with content). A re-fetch runs while the tab already has bets (e.g. re-entering the tab, reload()): an opaque overlay with its own spinner covers the list without dropping it, preserving scroll position.
  • Empty (signed in, no bets). Authenticated, zero bets on the active tab: stadium empty state ("Empty stands… / Place a bet and light up the game.").
  • Populated: bet cards render, infinite-scroll loads more at ~70% scroll depth until a short page marks the tab exhausted. Each leg's header shows, on the right, one of three things: its status badge (Won, Lost, …) once the leg is settled; the LIVE pill, with the match clock in front when showLiveClock is on, while its game is in play; otherwise its kick-off date and time.
  • Cash out. The cash out row appears on a card only while the stream has a value for it. Before the first message, after a stream error, or when the provider sends 0, there is no row, and the rest of the card is unchanged. The dialog has its own states: a loading label (Processing Cash Out) on the buttons while the request runs, a success step and an error step (see Cash out).
  • Render error. Caught by the widget's error boundary: an inline banner with a retry button; auto-clears after 8 seconds or on retry.

Hosted-mode note: when embedded inside a betslip's My Bets tab, <goma-my-bets> mounts without its own Shadow DOM: the surrounding betslip provides the CSS isolation, and this widget renders straight into it. The states above are unaffected; only style adoption changes (see Theming). The exception is a page with two copies of @gomagaming/core, where the embedded element can mount standalone instead (see Integrates with).

Theming ​

Override CSS variables via el.theme = { … }: the prefix --goma- is added automatically.

js
el.theme = {
  backgroundBetslip: '#171a26',   // Panel background
  backgroundPrimary: '#1f2435',   // Card background
  backgroundSecondary: '#252a3e', // Leg background
  textPrimary: '#ffffff',
  textSecondary: '#8c919c',
  highlightPrimary: '#29f2ff',    // Active sub-tab ring
  highlightSecondary: '#8750ff',  // Hover sub-tab ring
  separatorLine: '#3d425a',
  myTicketsWon: '#21ba45',        // Won strip / footer tint
  myTicketsLost: '#ed4f63',       // Lost strip / footer tint
  myTicketsOther: '#8750ff',      // Void / cashed-out strip / footer tint
}

Setting el.theme writes CSS custom properties as inline styles directly on the <goma-my-bets> element itself, so it works identically whether the element is standalone or embedded inside a betslip.

Tokens used by this widget ​

TokenElement
--goma-backgroundBetslipPanel shell background
--goma-backgroundPrimaryBet-card background
--goma-backgroundSecondaryPer-leg background inside a card; background of the partial cash out button
--goma-backgroundCardsCash out dialog cards: the amount field, the amount rows and the ? explanation
--goma-backgroundBorderBet-card border
--goma-pillsSub-tab pill background
--goma-textPrimary / --goma-textSecondaryBody text / muted labels. In the cash out dialog: main text and amounts / secondary text and the ? button
--goma-highlightPrimaryActive sub-tab ring; filled cardRadio circle. In cash out: background of the Cash out button and the dialog main buttons, the slider fill, the Close link, the focus ring, and the border of the paid amount row on the success step
--goma-highlightPrimaryContrastInner dot of a filled cardRadio circle; LIVE pill label and icon; text on the cash out buttons
--goma-liveTagLIVE pill background on a live leg
--goma-highlightSecondaryHover sub-tab ring
--goma-separatorLineDivider lines (sub-tab row, per-leg dividers). In cash out: borders of the partial button and of the dialog cards, the line above the remaining bet amount, and the slider track
--goma-myTicketsWonWon status strip + footer tint
--goma-myTicketsLostLost status strip + footer tint
--goma-myTicketsOtherVoid / cashed-out status strip + footer tint
--goma-alertErrorRender-error banner background
--goma-dialogBackgroundCash out dialog panel background

Cash out dialog in hosted mode: the dialog shell CSS (the panel, its backdrop and its transitions) is the shared dialog stylesheet, and in hosted mode it comes from the betslip that embeds this widget. Standalone, this widget adopts it itself. The look is the same.

Hosted-mode note: this widget's own default values for the tokens above live in its :host stylesheet (styles/my-bets.css), which is only adopted when <goma-my-bets> owns its own Shadow DOM (standalone mode). Embedded inside a betslip's My Bets tab, it mounts in hosted mode with no Shadow DOM of its own: its :host defaults are never adopted, so any of these tokens the betslip's own stylesheet doesn't already define fall back further, to @gomagaming/core's base theme (which uses different default colours than this widget's own :host defaults). In standalone mode the opposite happens: the widget's own :host defaults apply, and they win over --goma-* values the betslip host inherits from a page stylesheet or sets through the betslip's appearance (this widget has no appearance API, so it cannot be forwarded). A host theming <goma-my-bets> through a betslip should rely on explicit theme values. Set on the betslip and forwarded to the embedded element, or set on the embedded element directly. Rather than assuming this widget's own default palette applies. The same gap applies to the min-height: 360px sizing floor documented under Element tag: that rule also lives in the :host stylesheet, so it too is never adopted in hosted mode; an embedded <goma-my-bets> with no definite height relies entirely on the parent betslip's own layout rather than this widget's fallback.

i18n / message overrides ​

Override with config.messages:

js
el.config = {
  /* … */
  messages: {
    en: { my_bets_empty_authed_title: 'No bets yet' },
    fr: { my_bets_empty_authed_title: 'Aucun pari' },
    pt: { my_bets_empty_authed_title: 'Sem apostas' },
  },
}
Keyen defaultUsed for
my_betsMy BetsSub-tab row aria-label
my_bets_empty_guest_title / _subtitleOffside! / Log in to view your bets.Guest empty state
my_bets_empty_authed_title / _subtitleEmpty stands… / Place a bet and light up the game.Signed-in, no-bets empty state
open / closed / wonOpen / Closed / WonSub-tab labels
bet_idBet IDCard header, placed-date line
single_bet / multiple_betSingle Bet / Multiple BetCard header, by selection count
won / lost / cashed_out / draw / void (+ open, reused)Won / Lost / Cashed Out / Draw / Cancelled / OpenBet + per-leg status label
total_oddsTotal OddsCard footer
bet_amountBet AmountCard footer
possible_winningsPossible WinningsCard footer (pending bets)
returnedReturnedCard footer (won bets)
oddsOddsPer-leg odds row label
liveLiveLIVE pill on a live leg (shown upper-case)
loading_betslipLoading betslipSpinner sr-only label (first load + refresh overlay)
retryRetryError-banner retry button
cashoutCash OutCash out button label, amount row label
partial_cashoutPartial CashoutSlider step title
partial_cashout_tooltipCash out a portion of your bet early and leave the rest active until the event ends.? tooltip text
enter_or_drag_to_set_your_cashout_amountEnter or drag to set your cash out amountHint beside the amount field
choose_a_cash_out_amount(existing key)Number field, slider and partial button aria-label
min / maxMin. / Max.Slider limits
my_bets_remaining_bet_amountRemaining Bet AmountSlider step line
confirm_full_cashout / confirm_partial_cashoutConfirm Cashout? / Confirm Partial Cashout?Confirm step title
confirm_full_cashout_bodyYou are about to cash out your entire betConfirm step text
cancel / confirm / close / done(existing keys)Dialog buttons
processing_cashoutProcessing Cash OutButton label while the request runs
cashout_completed / partial_cashout_completedCashout Completed! / Partial Cashout Completed!Success step title
remaining_stakeRemaining StakeSuccess step line
my_bets_partial_cashed_out / my_bets_remaining_amountPartial Cashed Out / Remaining AmountCard cash out box after a partial cash out
my_bets_partial_cashout_confirm_amount / my_bets_partial_cashout_confirm_remainingYou are about to cash out {amount} / {amount} will remain in playPartial confirm step text
more_info(existing key)? button aria-label
cashout_unsuccessful / cashout_unsuccessful_descriptionCashout Unsuccessful / We couldn't process your request right now. Your bet is still active.Error step
my_bets_cashout_return / try_againReturn / (existing key)Error step buttons
my_bets_stakeback_bonus_labelStakeback BonusRefund row label on a lost or half-lost ticket carrying a stakeback refund
my_bets_stakeback_tooltip_titleStakeback RefundedRefund row's info-control popover title
my_bets_stakeback_tooltip_description{refund} ({percentage} of your {stake} bet amount) was delivered to your account balance.Refund row's info-control popover body, single-tier ticket where the refund matches stake x rate
my_bets_stakeback_tooltip_description_no_rate{refund} was delivered to your account balance.Refund row's info-control popover body, no tier, more than one tier, or a refund below stake x rate (for example a capped refund)

These are the same keys the betslip widgets' embedded My Bets tab renders, since both surfaces share the MyBetsPanel / MyBetsCard components.

Embedding examples ​

Vanilla HTML ​

html
<div style="height: 600px; max-width: 420px;">
  <goma-my-bets id="mb"></goma-my-bets>
</div>
<script type="module">
  import '@gomagaming/my-bets'

  const el = document.getElementById('mb')
  el.config = {
    betsApiBaseUrl: 'https://bets-api.example.com',
    playerApiBaseUrl: 'https://player-api.example.com',
    ucsOperatorId: '2838',
    socketUrl: 'wss://sports-ws.example.com',
    socketRealm: 'sportsbook',
    locale: 'en',
  }
  el.sessionId = currentUser.sessionId
  el.addEventListener('goma:my-bet-select', (e) => console.log(e.detail.bet, e.detail.tab))
</script>

React ​

jsx
import '@gomagaming/my-bets'
import { useEffect, useRef } from 'react'

function MyBets({ sessionId }) {
  const ref = useRef(null)
  useEffect(() => {
    if (!ref.current) return
    ref.current.config = {
      betsApiBaseUrl: 'https://bets-api.example.com',
      playerApiBaseUrl: 'https://player-api.example.com',
      ucsOperatorId: '2838',
    }
    ref.current.sessionId = sessionId
  }, [sessionId])
  return (
    <div style={{ height: 600, maxWidth: 420 }}>
      <goma-my-bets ref={ref} />
    </div>
  )
}

Vue 3 ​

vue
<script setup>
import '@gomagaming/my-bets'
import { ref, onMounted } from 'vue'

const el = ref(null)
const props = defineProps(['session'])

onMounted(() => {
  el.value.config = {
    betsApiBaseUrl: 'https://bets-api.example.com',
    playerApiBaseUrl: 'https://player-api.example.com',
    ucsOperatorId: '2838',
  }
  el.value.sessionId = props.session.id
})
</script>

<template>
  <div style="height: 600px; max-width: 420px;">
    <goma-my-bets
      ref="el"
      @goma:my-bet-select="(e) => console.log(e.detail)"
    />
  </div>
</template>

Accessibility ​

ElementRoleAttributes
Sub-tab rowtablistaria-label (localised "My Bets")
Sub-tab (Open / Closed / Won)button role="tab"aria-selected reflects the active tab
Bet card (cardSelectable: true, the default)article role="button"tabindex="0", aria-label (bet type + status), Enter / Space activate
Radio circle (cardRadio: true)-aria-hidden="true": purely decorative; the card itself carries the accessible role/label above
LIVE pill on a live leg-the localised "Live" text is read; its ring icon is aria-hidden="true"
Loading spinner-role="status", aria-busy="true", sr-only label
Retry (error banner)buttonlocalised text
Cash out row buttonsbuttonThe partial button has aria-label (localised "Choose a cash out amount"); its slider glyph is aria-hidden="true". The Cash out button is disabled when there is no value.
Cash out dialogdialog role="dialog"aria-modal="true", aria-labelledby points to the step title. The browser traps focus and Escape closes it. Focus returns to the button that opened it.
Amount number field and sliderinput type="number" / input type="range"Both have aria-label (localised "Choose a cash out amount"); the slider has aria-valuetext with the formatted amount.
? disclosurebuttonaria-label, aria-expanded and aria-controls point to the explanation text.
Step illustrations-alt="" and aria-hidden="true": decorative.

Integration Caveats ​

  • SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory.

  • Tokens/Theming: themeTokens.js was missing 4 tokens that are both :host CSS defaults and documented as used; added.

  • Initialization: flagBaseUrl has no observable effect (the flag glyph resolves from a fixed CDN constant); documented as currently inert.

  • Sizing, documented that the min-height:360px fallback is not adopted in hosted mode (embedded inside a betslip's My Bets tab, which has no shadow root of its own), previously the hosted-mode note covered color tokens but not this.

  • Initialization: this widget shares @gomagaming/core's i18n singleton with every other widget on the page; mounting two widgets with different locale values on one page is not currently isolated per instance (a known constraint, not fixed in this pass).

  • 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.11
  • A session-aware widget that is mounted again later in the same page load (for example favourites after the player opens an event and comes back) now keeps the valid session. Before, when the host set sessionId after the mount, the widget skipped the session check and stayed signed out, and favourites showed "Log in" after 15 s.
0.37.9
  • My Bets: the sub-tab pill labels (Open, Closed, Won, Cash Out) are now 12px instead of 14px. On a 320px panel, in English and in longer languages such as Lithuanian, at least part of the Cash Out pill now shows, so the player can see there is more to scroll to. This also applies to the My Bets tab inside both betslip widgets.
0.37.8
  • My Bets: a team's score now always stays on one line next to the team name. A long score, such as a cricket score in its second innings ("280 / 10 & 0 / 0"), no longer breaks onto two lines; the team name is cut short instead. The selected outcome is also shown in full when it fits, and the market label is cut short first. A very long outcome is cut short at 60% of the row.

    Core: the shared widget stylesheet gains the rule for this cap.

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.

  • Playground defaults now target the EveryMatrix demo prod operator (2879): the betslip place-bet endpoint and the My Bets operator ID. The published widgets do not change.

0.37.4
  • My Bets now shows the score and the live state of each game on a bet, in <goma-my-bets> and in the My Bets tab of <goma-betslip-sidebar> and <goma-betslip-floating>.

    • A live game on an open bet shows its live score. Before, every live leg in a betslip's My Bets tab showed -, because the embedded <goma-my-bets> never opened its live-score feed when it mounted inside a betslip.
    • A live leg now shows a LIVE pill (the live event card's tag) in place of its kick-off date. A per-event status feed decides when the game is in play, so a bet placed before kick-off also gets the pill and the score once the game starts, even while the list stays open.
    • New opt-in prop to show the match clock (for example 33' 1st Half) in front of the LIVE pill: showLiveClock / show-live-clock on <goma-my-bets>, and myBetsShowLiveClock / my-bets-show-live-clock on both betslips, which forward it. Off by default.
    • A settled leg whose bet record has no result now gets its final score: the widget asks for it once over the socket. A leg that already has a result is not changed.
    • @gomagaming/sports-domain adds betting.getEventPartScore(eventId) (a one-time score request), betting.subscribeToEventStatus({ eventId, onUpdate }) (the per-event status feed) and myBetsLiveStatus(event).
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).

  • Fix <goma-my-bets> crashing with "Cannot read properties of undefined (reading '_s')" inside the betslip's My Bets tab when a page loads two copies of @gomagaming/core, for example the betslip from the CDN and a chat widget bundled from npm. A widget nested in another widget only mounts hosted (in the parent's Vue app and Pinia) when both come from the same copy of core. A child from another copy now mounts standalone with its own Pinia.

    Such a child keeps its lifecycle events (ready, goma:connection, goma:connected, goma:session-ready, goma:session-expired, and goma:error with code API_INIT_ERROR) inside the parent's shadow root, so the page does not see them twice. Its other events still reach the page, as from a hosted child.

    The betslip now also forwards locale (at mount and on change) and iconBaseUrl to the embedded <goma-my-bets>, and calls its reload() after a placement when it mounted standalone, because it cannot see the betslip's store.

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
  • A My Bets card's footer now reads as one aligned block.

    The rule under the Total Odds / Bet Amount / Possible Winnings headings sat inside a narrow column that capped those figures well short of the card, so it read as a short underline beneath the headings rather than as a divider. It now runs from one card edge to the other, and the figures spread to the width of the card instead of staying bunched in the middle.

    The win-boost row is aligned to the same three columns: its label sits under Total Odds and its amount under the winnings figure it replaces, in the same size and typeface as the other figures rather than a smaller one pushed to the far edge.

  • A bet placed with an odds boost now shows it in My Bets.

    Until now a boosted ticket looked identical to any other: the winnings cell showed the boosted figure with nothing marking it as such. While the bet is open, the card now strikes through the pre-boost winnings figure and adds a "WIN BOOST (x%)" row beneath it carrying the boosted amount. A settled bet shows its settlement with no strikethrough: 0.00 when lost, the cashout amount when cashed out, the payout when won. The row stays on a won bet with that payout, and is hidden on a lost or cashed-out bet, where the boosted figure is money the player never received.

    The bets feed ships a different shape from the place-bet response: oddsBoost is a plain boolean rather than the boosted total, and the pre-boost winnings figure isn't sent at all. It's derived from the stake and the ticket's combined odds.

    The bet feed's mapper now carries the boost rate through to the card. It previously dropped the field, so the information never reached the card at all.

    A boosted ticket whose rate is missing from the feed now reads as the plain boost name rather than a truncated "WIN BOOST" with an empty bracket.

  • Bonus rates are shown as whole percentages again.

    A bonus paying a fractional rate was labelled with its exact figure, which differed from the figure the betting provider's own clients show for the same bonus. Rates now round to a whole percent everywhere they appear, so the betslip, the bonus list and the bet card agree with each other and with the provider.

    The one exception is a rate under half a percent, which would otherwise round to "0%" and read as a bonus that pays nothing. It keeps one significant figure instead, so 0.4% reads as "0.4%".

    This affects the label only. Every amount is calculated from the exact rate, so no figure a player is paid or shown as a total changes.

  • Stakeback bonus: the betslip now shows a progression indicator and a separate Stakeback row with the refund (min(stake x tier %, cap)) and the condition it is paid under, for example "Paid if exactly 1 selection loses". The refund is never added to the winnings. Stakeback is no longer offered in Single mode, where its tiers can never be reached, and no longer shows the shared bonus chip. My Bets shows the refund the provider paid on a lost ticket (betStakeBack). New copy in en, fr and pt.

    Fixes: with two wallets of the same bonus type offered on a slip, the tracker now prices the wallet the player actually applied instead of always the first one of that type. The My Bets refund sentence no longer states the exact rate arithmetic when the reported refund is below stake x rate (a capped refund), falling back to the plain "delivered to your account balance" sentence instead. A stakeback tier with no cap for the wallet currency is no longer read as a zero cap: the refund shows the full stake x rate.

  • Moved every shared UI component out of @gomagaming/sports-domain into a new @gomagaming/ui package, which depends on it:

    • The game cards: RegularOdds and its live/prelive/outright/market variants, SkeletonCard and CountryFlag. Used by every widget that lists events.

    • The betslip family: BetslipPanel and its selection, summary and bonus pieces, and the GomaDialog modal shell. Used by the betslip floating and sidebar widgets.

    • The missions family: the missions cards, level rows, tags, empty state and mission-detail panels. Used by the missions hub and mission details widgets.

    • The promo-card family: the promo card shell, hero, competition row, kickoff time, CTA link and skeleton. Used by the highlight card and pros' choice widgets.

    MyBetsPanel and MyBetsCard moved into @gomagaming/my-bets, their only consumer. The betslip widgets keep showing them through <goma-my-bets>.

    No widget's public surface, markup or behaviour changes, and each widget renders the same components as before. @gomagaming/ui is installed automatically with the widgets that use it.

    @gomagaming/sports-domain no longer exports any component. It keeps the stores, API clients, composables and the promo-card auto-mode layer, and additionally exports useBetslipSystem, useBonusSessionGate, bonusRowTitle, convertOdd, formatRatePercent, formattedDate, formattedTime and resolveDateLocale. A host importing one of the moved components from @gomagaming/sports-domain directly should import it from @gomagaming/ui (or, for the MyBets pieces, use <goma-my-bets>) instead.

0.35.0
  • Bonus rows now show the right figure, in the right currency.

    A stake-back or odds-boost bonus was listed with a money amount it does not have. The provider reuses the wallet's amount field to carry the bonus's RATE on these types, so a bonus paying 5% back arrived as 0.05 and the picker offered it as "0,05€": five cents, for a bonus worth up to 1000. These rows now read the rate from the bonus's own terms and show it as a percentage, prefixed to the bonus name only when the name does not already carry it, since operators commonly name them that way already.

    Amounts that ARE money are now formatted in the currency the bonus states rather than always in euros. A player can hold a bonus in a currency their balance is not in, and the old behaviour labelled those incorrectly. Hosts supplying their own formatStake are unaffected (that still takes precedence) and a bonus that states no currency is shown as a plain number rather than being assigned one.

    A fractional rate is no longer rounded to a whole number. A bonus paying 12.5% was shown as "13%", a figure that appears nowhere in the terms the player agreed to. The same helper now formats the rate wherever it appears, so the betslip and the bet card cannot disagree about it.

    A bonus whose amount is zero is shown by name alone. The provider sends the amount as a number on some wallets and a string on others, and the string form was rendering as a confident "0,00 €".

  • A bet placed with a stakeback bonus now says so in My Bets.

    Until now a player could choose a bonus, have it applied and place the bet, and then find nothing on the bet card indicating the bonus was attached. The card now carries a tag reading the rate the bonus pays back, for example "5% Stakeback".

    The tag is shown in every settlement state, not only once a bet is settled: the bonus is a property of the bet rather than of its outcome, and a player needs to see it is attached while the bet is still running. Where a bet qualifies for several tiers, the highest is shown.

    The bet feed's mapper now carries the bonus terms through to the card. It previously dropped them, so the information never reached the card at all.

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

  • A sport with no bundled icon now falls back to a real glyph instead of the browser's broken-image icon.

    SbIcon resolves a bundled SVG by name and otherwise falls through to an <img>. For a sport id the feed ships before its artwork exists (e-Volleyball, in the sports carousel and in the "Sports available" list) that <img> 404s, and the old error handler only faded it to opacity: 0.2, which leaves the broken-image glyph on screen at 20%. A failed <img> is now dropped from the render instead.

    What stands in is the sport's family glyph: every e-sports category falls back to the e-sports icon, which is a real icon rather than "no icon". The family is read from the sport list the widget was already fed (the parent whose childrenIds names the category) so no id list has to be kept in step with the provider, and a flat feed that names no parent simply falls back to the neutral noicon glyph as before. The new sportIconNames helper in @gomagaming/core is what the sport tiles, the picker's trigger pill and the settings modal's sport row all resolve through.

    A host serving its own sport artwork keeps priority, and keeps it whether or not it configured a base URL: ${iconBaseUrl}/icons/sports/dark_mode/{iconId}.svg when one is set, and the origin-relative /icons/sports/dark_mode/{iconId}.svg when it is not. iconBaseUrl has no value that means "my own root" ('' and '/' both read as unset), so a host serving icons from its root keeps working with no configuration at all. A stand-in is considered only once that request has failed.

    Every widget that draws a sport glyph through SbIcon inherits the change, which is why they are all listed here: the two sports pickers, the settings modal, both top-competitions widgets, the sport banners, the game-details header, the suggested-bet legs, the My Bets cards, the two betslips that embed them, the highlight card and the Pros' Choice card.

    The family is looked up across the whole sport list the feed sent rather than the slice on screen. In rpc/subscribe mode useSportsSource mirrors the betting store filtered to the current context (LIVE vs popular), and a parent category carries no events of its own, so it is routinely missing from that slice: every e-sports child would have fallen back to the neutral glyph. The unfiltered list is mirrored alongside it as state.sportsCatalog, and sportIconLookupList (new in @gomagaming/core) is what the tiles, the trigger pill and the settings row read. mapSport also refreshes childrenIds on an existing entry: the list was written only when the entry was first created, so a family relation that arrived on a later dump, or a child added to one. Never reached the widget.

0.34.0
  • Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default

    A <goma-region-filter> configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached <goma-competitions-list>, which groups its competitions by the same region data.

    The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport 1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.

    That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.

    Separately, every numeric property on every widget (sportId, skeletonCount, pageSize, spaceBetween, autoplayInterval and the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value. el.sportId = '8' therefore left the widget on sport 1 and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null, undefined, '', booleans, objects, NaN, Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and the config object were never affected by this and are unchanged.

0.33.0
  • My Bets cards (the <goma-my-bets> widget, also nested inside the betslips) title a placed system bet "System Bet" instead of "Multiple Bet". The title now follows the provider bet type (SINGLE / MULTIPLE / SYSTEM, any casing), treats an unrecognised type that carries a systemBetType as a system, and only falls back to the leg count when neither is present. Portuguese now reads "Aposta Sistema" for it instead of the untranslated English string.
0.32.0
  • Widgets no longer dispatch a bare error event alongside goma:error.

    Every factory-built widget's app-level Vue error handler dispatched two events for the same failure: goma:error, and an un-namespaced error. The bare one is gone; goma:error is unchanged and still carries the same { message, code, component? } detail.

    Like every GomaPulse event it was composed and bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had on window.addEventListener('error', …): as a CustomEvent with message, error and filename all undefined, since those are ErrorEvent fields a CustomEvent does not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.

    That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its errorCaptured walk at the throwing component's parent, so the onErrorCaptured boundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emit goma:retry.

    Hosts listening for goma:error need no change. A host listening for bare error on a widget element should move to goma:error; it was documented as unreachable, so in practice it delivered nothing until now.

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.26.0
  • Stop publishing source maps and package source.

    dist/*.js.map was emitted by the shared Vite preset and swept into every tarball by files: ["dist/", "src/"], with the original source inlined in sourcesContent. src/ shipped the same source again, in the clear. Both are one URL away from a browser once a package is served from a CDN, so the preset now builds with sourcemap: false and files no longer carries src/.

    @gomagaming/core keeps the two paths its export map actually points at: src/styles/main.css and src/types/index.d.ts. Its tarball drops from 1741 files to 576, which also brings it back under the 900-file limit a per-request unpacker can handle.

    Deep imports of @gomagaming/<pkg>/src/* from an installed package no longer resolve. Nothing documented used them; imports inside the monorepo are unaffected.

0.24.0
  • my-bets: add an opt-in card-radio prop (JS cardRadio) that renders a single-select radio affordance in each bet card's header. An empty circle by default, filled with highlightPrimary when that card is the panel's selected bet. It is a dedicated flag, independent of card-selectable, so enabling it never changes the standalone widget's default tap behaviour. Built for select-then-confirm bet pickers.
0.22.0
  • Session: new goma:session-ready event. The host can finally tell when a session is usable

    A host knows when it issued a sessionId. It has had no way to know when that id became something the player can bet with, and those are seconds apart: the REST probe resolves the userId, then the CE-login exchange fetches a ceSession and binds the WAMP socket, and only then does the place-bet payload carry the username the provider rejects a bet without.

    A host that reveals its betting UI on its own login response therefore hands the player a Place Bet button whose first submission is guaranteed to fail: tap once, get an error, tap again, it works.

    goma:session-ready closes that gap, as the counterpart to the existing goma:session-expired:

    js
    window.addEventListener("goma:session-ready", ({ detail }) => {
      // detail: { userId, username }
      closeLoginModal();
    });

    Emitted by every session-aware widget (both betslips, favourites, my-bets) once per resolved token, and again whenever a new token is minted (max-age refresh, WAMP reconnect), so handlers must be idempotent.

    There is no host-side alternative: the shared goma:session:v1 envelope is public, but storage events fire only in other tabs, never the writer's, so a host could only have polled it.

  • Session: a player who signs in without reloading can bet again

    Signing in during a page's lifetime left the player half-authenticated: the place-bet payload went out with no username and the provider refused it, and only a page reload fixed it. Two defects, each of which was hiding the other.

    A refused CE-login was committed as a session. A successful loginWithCeSession answers { username, sessionToken, message }; a refused one answers a single-key payload, which is still truthy, so the guard let it through. That wrote status: 'valid' with a null username and a null sessionToken: an envelope every widget reads as resolved, that can never place a bet, and that nothing re-triggers. A response missing either credential is now treated as a failed attempt and retried.

    An unresolved session was mistaken for a logout. While the exchange had committed nothing, the shared envelope stayed idle, and the mirror read that as "the player signed out" and wiped the userId the REST probe had already established, so the bet went out with no identity at all and came back 401. idle now only tears the identity down when the host has actually dropped its sessionId, which is what a real logout does.

    Fixing either alone made things worse, because the first defect's bogus commit was what kept the second from firing.

0.20.1
  • Fix: a 401 no longer signs the player out of the whole page

    0.20.0 made a dead session sign the player out, but treated any 401/403 from place-bet or favourites as proof the session had died, and applied the logout page-wide regardless of which credential actually failed. In practice that made a signed-in session unusable: place a bet, get signed out.

    A bare 401 is no longer read as a dead session. It means "this request wasn't authorised", which equally covers a gateway rejection, a misrouted call, or a request issued with a credential the host has already replaced. Place-bet and favourites now require the provider's explicit InvalidSession marker before condemning the session, matching the production sportsbook's handleSessionExpiration. Anything else fails that one request and leaves the session alone. The session probe (GET /v1/player/session/player) and the balance stream's SESSION_EXPIRATION event remain the authorities on whether the player is still signed in.

    A rejection is scoped to the credential it names. markInvalid() now records which sessionId failed, and the logout + goma:session-expired only apply to a widget still running that one. Previously the death was stamped onto whatever the envelope currently held, which, after a re-login, is the new token, so a late 401 from the old one killed the session that had just replaced it. Requests can't be recalled: a /balance call, a favourites poll, or an SSE stream replaying its captured headers on backoff can all outlive the token they were issued with.

    commit() no longer blanks fields it wasn't given. sessionId used ??, so an empty string overwrote a real id: hosts feeding a pre-resolved identity (userId + sessionToken, no raw sessionId) wiped the id every other widget had recorded, after which isValidFor could never match and the CE-session exchange re-ran indefinitely. username had the same flaw: only the CE-login exchange learns it (the REST probe never returns one), so any other committer nulled it, and username then silently vanished from every subsequent place-bet payload. Both are preserved now; a real logout still clears them via clear().

    A spent rejection left over from a previous sessionId is deliberately left in the envelope rather than cleared: clear() resets it to 'idle', whose mirror branch wipes the identity the probe just established. Trading one logout bug for another. It is inert, since every consumer scopes on the id it names.

0.20.0
  • A dead session now signs the player out and notifies the host

    A place-bet rejected with 401 used to paint the literal string HTTP 401 into the betslip panel: the transport fallback, because the provider sends no body on a gateway rejection and HTTP/2 carries no statusText. Above it sat a "your session is no longer valid" banner, and underneath, nothing recovered: the invalidation cleared the shared token but no resolver watched for it, the stale token stayed on the user store, and Place Bet stayed enabled. Every retry re-failed identically until a full page reload.

    gomawt cannot re-authenticate (the REST sessionId is minted by the host's login and the provider exposes no refresh path) so the fix hands the problem to the party that can solve it.

    A dead session is now a logout, not a warning. The identity is cleared, so balance, My Bets and favourites all fall back to their guest states. The warning banner and its session_invalid_warning message key are removed: leaving the UI presenting a signed-in player who couldn't do anything was the worst of both. A place-bet 401/403 also renders no error text. goma:bet-failed still carries the full status/body for telemetry.

    goma:session-expired is the session-death contract. Every authoritative rejection funnels through useSharedSession().markInvalid(reason) and reaches the host: place-bet 401/403, favourites 401/403, the session probe, the CE-login exchange, and the SSE expiry that already fired it. detail.reason names the source. It fires once per invalidation per mounted session-aware widget, and again on a retry, so handlers must be idempotent.

    Migrating an existing host ​

    This changes what the widget shows, so a host upgrading from an earlier version should check both of these:

    1. The session-invalid banner is gone. If you relied on the widget painting "Your session is no longer valid" as the only signal to the player, there is now no widget-side surface at all: a dead session silently signs them out. Listen for goma:session-expired and show your own message.
    2. Place-bet errors no longer include session failures. A 401/403 writes nothing to the panel. If you were reading the rendered error text, switch to the goma:bet-failed event, which still carries status and body.
    js
    let lastHandled = null;
    el.addEventListener("goma:session-expired", async (e) => {
      const { reason, invalidationId } = e.detail;
      if (invalidationId === lastHandled) return; // a sibling widget already reported it
      lastHandled = invalidationId;
      el.sessionId = await myApp.reauthenticate();
    });

    Hosts using placeBetMode: 'event' are unaffected: they submit through their own backend and already own auth.

    detail.invalidationId is a monotonic counter: every mounted session-aware widget reports the same id for one death, a retry reports a higher one. Dedupe on it rather than on your own logged-in flag, which would swallow the retry.

    A page load re-verifies the session instead of trusting the cache. The goma:session:v1 envelope records what gomawt last resolved, never that the provider still honours it, so a session killed server-side stayed "valid" across reloads for the whole max-age window. The first sessionId of each page load now always runs the REST probe and always re-runs the CE-session exchange (the cached token was bound to the previous socket). Both are de-duped page-wide (one probe plus one exchange per load however many session-aware widgets are mounted) after which the normal sessionMaxAgeMs reuse window resumes.

    requireSession now gates on a usable session, not just a non-empty string. A stale sessionId restored from a host cookie satisfied the old check, so Place Bet stayed enabled and the submit went out to a guaranteed 401. The widget can't clear the host-owned sessionId prop, so this is also what stops a player re-tapping into the same failure while the host gets around to signing them off. The gate is scoped to the id the rejection was recorded against, so a freshly-issued token isn't blocked by the previous one's death.

    Transient failures change nothing. A network blip, a 5xx, or a missing playerApiBaseUrl leaves the session exactly as it was (no logout, no page-wide invalidation, no event) so a flaky connection can't sign the player out. Only an authoritative rejection does.

0.16.0
  • Add <goma-my-bets> standalone My Bets widget (@gomagaming/my-bets): renders the user's bets (Open / Closed / Won) with live match scores on open bets over WAMP, and emits goma:my-bet-select on card tap. Shared sports-domain additions: subscribeToEventPartScores + parseEventPartScore (per-event live-score feed), useMyBetsScoresLive, a reloadToken force-reload hook in useMyBets, an eventResult field on mapped legs, and a card-tap select emit on MyBetsCard / MyBetsPanel.

    The betslip widgets (@gomagaming/betslip-sidebar, @gomagaming/betslip-floating) now reuse the standalone <goma-my-bets> widget in their MyBets tab instead of embedding the panel directly: one MyBets implementation, shared. They take @gomagaming/my-bets as a dependency and no longer run their own useMyBets() feed (the widget owns it).