<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-betsYou also need the shared peer dependencies installed at your app level:
bash
pnpm add vue pinia vue-router vue-i18n @vueuse/core autobahn-browservue, 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 */ } },
}| Key | Fallback chain | Purpose |
|---|---|---|
betsApiBaseUrl | → bettingApiBaseUrl → apiBaseUrl | Base URL for the bets-api feed (open-bets / settled-bets). It also serves the cash out value stream and the cash out request. |
playerApiBaseUrl | → apiBaseUrl | Base 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).
Session probe: setting
sessionId(withconfig.playerApiBaseUrlconfigured) triggersGET {playerApiBaseUrl}/v1/player/session/playerwith headerX-SessionId. A response shaped{ IsAuthenticated: true, UserID }populates the per-instance user store (userId,isAuthenticated); any failure, non-200, orIsAuthenticated: falseclears the session and the widget falls back to the guest empty state.Bets feed: once authenticated (and
ucsOperatorId+betsApiBaseUrlare 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-idheaders and cursor-paginate vialimit=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 fromnow(). The fourth sub-tab, Cash Out (cashout→settled-bets?betStatus=CASHED_OUT), lists bets that were fully cashed out. It shows only whilecashoutEnabledis on (see Cash out).- Open →
Live open-bet scores: when
config.socketUrlis set, the widget opens a per-eventeventPartScores/smallWAMP 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 thatshowLiveClockputs 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-apiisLiveflag 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 itseventResult; when that field is empty, the widget asks the sameeventPartScores/smalltopic once for the final score and shows it on the leg. WithoutsocketUrl, 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-apiisLiveflag alone. There is no error state or degraded UI for the missing socket; it's a silent, expected configuration.Cash out values (stream): while
cashoutEnabledis 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 auserIdheader. 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 of0,nullor 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 turnscashoutEnabledoff, 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.Cash out request: confirming the dialog sends
POST {betsApiBaseUrl}/bets-api/v1/{ucsOperatorId}/cashoutwith the same headers. The body hasbetId,cashoutValue,cashoutType('FULL'or'PARTIAL'),cashoutChangeAcceptance: 'WITHIN_THRESHOLD',operatorIdandlanguage. A'PARTIAL'request also haspartialCashoutStake. 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 havesuccess: true. A missingsuccessflag is a failure. The paid amount is the first number found incashoutPayout,cashoutValueorcashoutAmountof the answer, or the quoted value when the answer has none. When the answer haspendingCashOut: true, the provider accepted the cash out but holds it for approval. A401or403here 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
| Property | Attribute | Default | Description |
|---|---|---|---|
sessionId | session-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. |
ucsOperatorId | ucs-operator-id | '' | Operator id. Can also be set via config.ucsOperatorId; either surface works. |
myBetsEmptyGuestImage | my-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. |
myBetsEmptyAuthedImage | my-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. |
iconBaseUrl | icon-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. |
flagBaseUrl | flag-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. |
reloadToken | reload-token | 0 | Bump 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. |
cardSelectable | card-selectable | true | Gates 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. |
cardRadio | card-radio | false | Opt-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. |
showLiveClock | show-live-clock | false | Shows 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. |
cashoutEnabled | cashout-enabled | true | Offers 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. |
subTab | sub-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. |
myBetsCashoutWarningImage | my-bets-cashout-warning-image | '' | URL of the illustration on the cash out confirm step. Nothing renders when unset. |
myBetsCashoutSuccessImage | my-bets-cashout-success-image | '' | URL of the illustration on the cash out success step. Nothing renders when unset. |
myBetsCashoutErrorImage | my-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
| Status | Event | Detail | Emitted when |
|---|---|---|---|
| Canonical | ready | {} | Widget mounted, initial render done. |
| Canonical | goma: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. |
| Canonical | goma: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. |
| Canonical | goma: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. |
| Canonical | goma: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. |
| Canonical | goma: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. |
| Canonical | goma: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. |
| Canonical | goma: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
localeso a language switch still reaches it, and callsreload()after each placement, because the sharedlastPlacedBetstore 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, andgoma:errorwith codeAPI_INIT_ERROR) stay inside the betslip's shadow root, so the page sees each of them once, from the betslip. Its other events (for examplegoma:my-bet-select) reach the page as they do in hosted mode. - It adopts its own
:hoststylesheet, 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
| Topic | Detail |
|---|---|
| What shows | A tag with the rate the bonus pays back, for example 5% Stakeback |
| When | In 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 tiers | The highest tier is shown |
| Feed field | stakeBackOptions.sportsStairs |
| Copy | my_bets_stakeback_tag ({percentage} Stakeback). You can override it like any other message, see i18n. |
| Refund row | On 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 source | betStakeBack 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:
| Status | Winnings figure | Boost row |
|---|---|---|
| WON | The payout | Shown, with the same payout |
| LOST | 0.00 | Hidden |
| CASHED_OUT | The cashout amount | Hidden: 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)oddsBoostPercentageamountandtotalPriceValue(the combined odds). The feed does not send the pre-boost winnings, so the widget derives it asamount × 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:
| Status | Winnings figure |
|---|---|
| Open | potentialNetReturns; when the feed has none, max(0, amount × (totalPriceValue − 1)) |
| WON, HALF_WON, VOID, CANCELLED, DRAW, PUSHED | the 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_LOST | 0.00 |
| CASHED_OUT | The 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.
| Step | What 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. |
| Confirm | The 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. |
| Success | The 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. |
| Error | The 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, elseamountCashOut). - Possible Winnings is always the provider's
currentPossibleWinning(elsemaxWinning). 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
cashoutEnabledturnsfalse, an open dialog closes. - The widget emits
goma:cashout-successorgoma:cashout-erroronce 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.
reason | When |
|---|---|
not-signed-in | The widget has no session, user, operator id or bets-api base. |
invalid-stake | A partial cash out with no usable stake. |
network | The request failed with no answer (offline, blocked). |
http-error | The provider answered with a non-2xx status. status holds the number. |
rejected | The provider answered 2xx but not with success: true. |
unknown | Any 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 aCASHED_OUTbet 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
showLiveClockis 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
| Token | Element |
|---|---|
--goma-backgroundBetslip | Panel shell background |
--goma-backgroundPrimary | Bet-card background |
--goma-backgroundSecondary | Per-leg background inside a card; background of the partial cash out button |
--goma-backgroundCards | Cash out dialog cards: the amount field, the amount rows and the ? explanation |
--goma-backgroundBorder | Bet-card border |
--goma-pills | Sub-tab pill background |
--goma-textPrimary / --goma-textSecondary | Body text / muted labels. In the cash out dialog: main text and amounts / secondary text and the ? button |
--goma-highlightPrimary | Active 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-highlightPrimaryContrast | Inner dot of a filled cardRadio circle; LIVE pill label and icon; text on the cash out buttons |
--goma-liveTag | LIVE pill background on a live leg |
--goma-highlightSecondary | Hover sub-tab ring |
--goma-separatorLine | Divider 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-myTicketsWon | Won status strip + footer tint |
--goma-myTicketsLost | Lost status strip + footer tint |
--goma-myTicketsOther | Void / cashed-out status strip + footer tint |
--goma-alertError | Render-error banner background |
--goma-dialogBackground | Cash 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' },
},
}| Key | en default | Used for |
|---|---|---|
my_bets | My Bets | Sub-tab row aria-label |
my_bets_empty_guest_title / _subtitle | Offside! / Log in to view your bets. | Guest empty state |
my_bets_empty_authed_title / _subtitle | Empty stands… / Place a bet and light up the game. | Signed-in, no-bets empty state |
open / closed / won | Open / Closed / Won | Sub-tab labels |
bet_id | Bet ID | Card header, placed-date line |
single_bet / multiple_bet | Single Bet / Multiple Bet | Card header, by selection count |
won / lost / cashed_out / draw / void (+ open, reused) | Won / Lost / Cashed Out / Draw / Cancelled / Open | Bet + per-leg status label |
total_odds | Total Odds | Card footer |
bet_amount | Bet Amount | Card footer |
possible_winnings | Possible Winnings | Card footer (pending bets) |
returned | Returned | Card footer (won bets) |
odds | Odds | Per-leg odds row label |
live | Live | LIVE pill on a live leg (shown upper-case) |
loading_betslip | Loading betslip | Spinner sr-only label (first load + refresh overlay) |
retry | Retry | Error-banner retry button |
cashout | Cash Out | Cash out button label, amount row label |
partial_cashout | Partial Cashout | Slider step title |
partial_cashout_tooltip | Cash 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_amount | Enter or drag to set your cash out amount | Hint beside the amount field |
choose_a_cash_out_amount | (existing key) | Number field, slider and partial button aria-label |
min / max | Min. / Max. | Slider limits |
my_bets_remaining_bet_amount | Remaining Bet Amount | Slider step line |
confirm_full_cashout / confirm_partial_cashout | Confirm Cashout? / Confirm Partial Cashout? | Confirm step title |
confirm_full_cashout_body | You are about to cash out your entire bet | Confirm step text |
cancel / confirm / close / done | (existing keys) | Dialog buttons |
processing_cashout | Processing Cash Out | Button label while the request runs |
cashout_completed / partial_cashout_completed | Cashout Completed! / Partial Cashout Completed! | Success step title |
remaining_stake | Remaining Stake | Success step line |
my_bets_partial_cashed_out / my_bets_remaining_amount | Partial Cashed Out / Remaining Amount | Card cash out box after a partial cash out |
my_bets_partial_cashout_confirm_amount / my_bets_partial_cashout_confirm_remaining | You are about to cash out {amount} / {amount} will remain in play | Partial confirm step text |
more_info | (existing key) | ? button aria-label |
cashout_unsuccessful / cashout_unsuccessful_description | Cashout Unsuccessful / We couldn't process your request right now. Your bet is still active. | Error step |
my_bets_cashout_return / try_again | Return / (existing key) | Error step buttons |
my_bets_stakeback_bonus_label | Stakeback Bonus | Refund row label on a lost or half-lost ticket carrying a stakeback refund |
my_bets_stakeback_tooltip_title | Stakeback Refunded | Refund 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
| Element | Role | Attributes |
|---|---|---|
| Sub-tab row | tablist | aria-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) | button | localised text |
| Cash out row buttons | button | The 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 dialog | dialog 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 slider | input type="number" / input type="range" | Both have aria-label (localised "Choose a cash out amount"); the slider has aria-valuetext with the formatted amount. |
| ? disclosure | button | aria-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.jswas missing 4 tokens that are both:hostCSS defaults and documented as used; added.Initialization:
flagBaseUrlhas no observable effect (the flag glyph resolves from a fixed CDN constant); documented as currently inert.Sizing, documented that the
min-height:360pxfallback 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 differentlocalevalues 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, awaitwindow.__gomaCdn.readyfirst, or configure through attributes. See Setting a property before the widget's definition has loaded.
See Integration Caveats for the cross-widget picture.
Changelog
Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.
Every release that changed this widget, newest first. The latest is open; the rest expand.
0.37.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
sessionIdafter 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. SetcashoutEnabled(attributecashout-enabled) tofalseto turn it off. The widget emits two new events:goma:cashout-success(withbetId,cashoutType,cashoutAmount,pendingand, for a partial cash out,partialCashoutStake) andgoma:cashout-error(withbetId,cashoutType, areasonofnot-signed-in,invalid-stake,network,http-error,rejectedorunknown, and, when known, the HTTPstatusand the provider'smessage). Three new image properties,myBetsCashoutWarningImage,myBetsCashoutSuccessImageandmyBetsCashoutErrorImage, set the pictures on the confirm, success and error steps. When one is not set, that step shows no picture.A cash out counts as done only when the provider answers with
success: true.cashoutAmountis the amount the provider paid, or the quoted value when it sends none. When the provider accepts a cash out but holds it for approval,pendingistrue, the bet stays in the Open list and the list is fetched again.<goma-betslip-sidebar>and<goma-betslip-floating>forward the newmyBetsCashoutEnabledattribute (my-bets-cashout-enabled) and the three image properties to the My Bets tab. After a successful cash out, the betslip asks for a new balance at once and again after 2.5 seconds. In<goma-betslip-floating>, the cash out dialog can open over the betslip dialog, and closing it leaves the betslip open and does not move the page behind.<goma-live-chat>and<goma-messenger>turn cash out off in the My Bets list of their ticket share screens.@gomagaming/network:openSseaccepts amethodoption and abodyoption, so a stream can be opened with a POST request.openSsealso callsonClosewhen its retries run out after an error, so a consumer knows the stream is finished.@gomagaming/my-betsholds the cash out dialog logic and now depends on@gomagaming/ui.<goma-my-bets>has a fourth sub-tab, Cash Out, that lists fully cashed out bets only (a partly cashed out bet stays in Open). It shows only while cash out is on, a full cash out that is not pending moves the widget to it when the success step closes (a partial or pending one shows Open), a page that holds only hidden bets loads the next page by itself, andgoma:my-bet-selectcan now reporttab: 'cashout'. A typed partial amount is rounded down to cents, and Try again shows the current value before the player confirms. After a partial cash out, the card shows a Cashed Out tag next to the title and a box with the amount paid out and the stake still in play; the Bet Amount keeps the stake as placed. A fully cashed out card keeps the neutral look, shows no leg result and a box with the total paid out. Possible Winnings is always the provider's figure (the winnings without the stake on a free bet). The cash out dialog is centred on every screen size, up to 480px wide, the amount field shows the currency, and the ? opens a tooltip.@gomagaming/sports-domainhas the cash out API calls and the live value stream. The stream keeps the last values while it retries and opens again when the browser goes back online or the tab becomes visible. It exports two new composables,useMyBetsCashoutValuesanduseMyBetsCashout, and a newrequestBalanceRefreshaction on the user store.@gomagaming/coreand@gomagaming/uicarry the shared dialog and slider styles.GomaDialogin@gomagaming/uitakes an optionalpanelClasswith extra classes for the panel; when set, it also replaces the default border colour. In@gomagaming/core,useScrollLocknow does nothing for a lock that is taken while another lock is active.<goma-betslip-floating>now closes the dialogs that are open inside it (the bonus picker too) before it closes itself.<goma-my-bets>has a newsubTabproperty (attributesub-tab). The host sets which sub-tab opens,'open','settled'(or'closed'),'won'or'cashout', and can change it later. The player can still click any tab.<goma-betslip-sidebar>and<goma-betslip-floating>have a newmyBetsSubTabproperty (attributemy-bets-sub-tab) that sets the sub-tab inside the My Bets tab.cashoutEnabledon<goma-my-bets>andmyBetsCashoutEnabledon both betslips turn cash out off when the property is set to the string'false', the same as the attribute.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-clockon<goma-my-bets>, andmyBetsShowLiveClock/my-bets-show-live-clockon 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-domainaddsbetting.getEventPartScore(eventId)(a one-time score request),betting.subscribeToEventStatus({ eventId, onUpdate })(the per-event status feed) andmyBetsLiveStatus(event).
- A live game on an open bet shows its live score. Before, every live leg in a betslip's My Bets tab showed
0.37.3
Fix bet placement with the "Do not accept any changes in odds" setting. The betslip sent
oddsValidationType: 'ACCEPT_NONE', which is not a value the provider accepts, so every bet failed with error 147 "Invalid Odds Validation Type". It now sendsWITHIN_THRESHOLD, one of the three values the provider accepts. The provider then refuses any odds change when the account threshold is 0.Hosts in
placeBetMode: 'event'see the change too: thegoma:place-bet-requestdetail now carriesoddsValidationType: 'WITHIN_THRESHOLD'for this setting, where it carried'ACCEPT_NONE'before. A host that forwards the payload to its own backend, or checks this value, must acceptWITHIN_THRESHOLD. Thecustom-elements.jsontype of that field now lists it.Two more place-bet changes ride in the same release, and one display fix.
The place-bet payload now sends
freeBetas a boolean:trueonly when theubsWalletIdon that payload belongs to a free-bet wallet (so the provider books a free bet and not a cash stake),falseotherwise, where it sentnullbefore; thegoma:place-bet-requestdetail type changes fromnull|objecttoboolean. A betslip pick now setsubsWalletIdandfreeBettogether and wins over a hostplaceBetBody.freeBet; with no betslip pick a host that sends a free-bet wallet throughplaceBetBody.ubsWalletIdmust also sendfreeBet: true. The picked wallet's bonus type is now stored with the pick (and shared like the wallet id) as{ walletId, type }inselectedBonusTypeandselectedBonusTypeBySelectionof thegoma:betslip:v4envelope, and is used only for that same wallet id. A free bet pick on a Single selection stays flagged as a free bet, and is no longer cleared, when a refresh of that selection's wallets response leaves the wallet out. The two fields are additive and optional, so the envelope version does not change. In the same event detail type,ubsWalletIdis now absent instead ofnullwhen no bonus is picked, andoddsBoostPercentandcapAmountare listed.A Single bet can now be placed with an odds boost: the selection picker lists a boost that has reached a stair (it has a rate and a cap), leaves the typed stake alone, and the selection's payload carries the wallet id with the boost terms read from that selection's own wallets response. A boost with no usable stair is not offered on a Single selection, for fetched and host-fed wallets alike.
The betslip, My Bets and the chat ticket share now show a free bet's winnings without the stake. My Bets reads
potentialNetReturns(or stake times odds minus one) for every status except lost (0) and cashed out (unchanged), where it showed the stake as part of the winnings. The betslip shows the quote minus the stake for a Single selection or a Multiple slip carrying a free bet. ThepossibleWinningsofgoma:bet-placedkeeps the provider's figure, stake included. The chat ticket (possible_winnings) follows the same rule for a free bet.For hosts: in
goma:place-bet-requestand in the place-bet body,freeBetis now a boolean, and a betslip pick sets it. A host that used to put a free-bet token object inplaceBetBody.freeBetmust stop doing that. SendfreeBet: trueonly when you send your own free-bet wallet id and the betslip has no pick.@gomagaming/sports-domainhas two new exports:isFreeBetWallet(whether a wallet id belongs to a free-bet wallet) andfreeBetNetWinnings(a free bet's winnings without the stake).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, andgoma:errorwith codeAPI_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) andiconBaseUrlto the embedded<goma-my-bets>, and calls itsreload()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/quickAddAmountsJsonare nowplaceBetHeaders/placeBetBody/quickAddAmounts. - The time-filter and filters-settings ticks editors set
ticks. - live-chat's layout control writes
position, the config key the element reads. - The sports-navigation widgets drop the unread
sportsJson. - JSON-typed defaults are real objects and arrays instead of serialized strings. The top-competitions
highlightedImagesstring default was indexed character by character and requested bad image URLs on every load. The missions-hubemptyStateImagesillustrations now show.
No widget runtime behaviour changes.
- The betslips'
0.36.0
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:
oddsBoostis 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-domaininto a new@gomagaming/uipackage, which depends on it:The game cards:
RegularOddsand its live/prelive/outright/market variants,SkeletonCardandCountryFlag. Used by every widget that lists events.The betslip family:
BetslipPaneland its selection, summary and bonus pieces, and theGomaDialogmodal 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.
MyBetsPanelandMyBetsCardmoved 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/uiis installed automatically with the widgets that use it.@gomagaming/sports-domainno longer exports any component. It keeps the stores, API clients, composables and the promo-card auto-mode layer, and additionally exportsuseBetslipSystem,useBonusSessionGate,bonusRowTitle,convertOdd,formatRatePercent,formattedDate,formattedTimeandresolveDateLocale. A host importing one of the moved components from@gomagaming/sports-domaindirectly 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.05and 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
formatStakeare 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 throughtoLocaleDateString. Withlocale: 'en'(the bare tag, whichIntlresolves to American ordering) that long form came out asOctober 11next to the card's11/10. English was the only shipped locale that disagreed with its own cards (frandptare already day-first, and the shared time formatter has always been pinned toen-GB), so the long form now resolves a bareentoen-GB. A host that explicitly configuresen-USkeeps American ordering.Two surfaces build their own
Intl.DateTimeFormatrather than going through the shared formatters, and both showed the same inversion for the same match: the My Bets card's leg date (which also printed07 Octoberwhere the cards print7 October, and sat directly under a hand-builtdd/mm/yyyy"placed" line) and the live-chat bet modal's per-leg date. Both now resolve the locale the same way.Every package listed here renders a date that changes, which is why the note reaches all of their pages rather than only the two whose source moved: including the two betslips and
<goma-messenger>, which render the same leg date through the<goma-my-bets>widget they embed.Not covered, and deliberately: the missions surfaces format challenge dates with the browser's locale rather than the widget's, and, in the same file, render the same instant as a deliberately locale-independent
DD/MM/YYtwo functions below, so one view contradicts itself regardless of locale. That is a different defect from this one and wants its own ticket.A sport with no bundled icon now falls back to a real glyph instead of the browser's broken-image icon.
SbIconresolves 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 toopacity: 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
childrenIdsnames 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 neutralnoiconglyph as before. The newsportIconNameshelper in@gomagaming/coreis 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}.svgwhen one is set, and the origin-relative/icons/sports/dark_mode/{iconId}.svgwhen it is not.iconBaseUrlhas 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
SbIconinherits 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/subscribemodeuseSportsSourcemirrors 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 asstate.sportsCatalog, andsportIconLookupList(new in@gomagaming/core) is what the tiles, the trigger pill and the settings row read.mapSportalso refresheschildrenIdson 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,autoplayIntervaland 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 sport1and 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 theconfigobject 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 asystemBetTypeas 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
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes 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
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary 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 emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma: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'screateWidgetElement()factory, andlive-chat's andmessenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): theHTMLElementsubclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw aReferenceErrorimmediately.
Per-package fixes:
favorites: addedsrc/themeTokens.jsand a:hoststylesheet (previously the package had neither, so it silently rendered core's base theme instead of its documented dark-navy/cyan/amber theme).suggested-bet. Fixed a leaked WAMP subscription: switching a mounted widget'sdataSourceaway fromrpc/subscribenever released the open topic.filters-settings: fixed a dead:host{display:inline-block}CSS rule (the shared element factory's inlinedisplay:blockalways won), so the widget always rendered full-width instead of the intended compact inline trigger.game-details: added amin-heightfallback for the inert/no-eventIdstate in an unsized container.betslip-sidebar,my-bets,competition-filter,suggested-bet,events-horizontal,top-competitions,top-competitions-pills,competitions-list,multi-banners,casino-games,sports-navigation-horizontal. CorrectedthemeTokens.jsregistries that had drifted from actual token usage, added missinggoma:retryEvents-table rows, and other small doc/registry corrections.region-filter: added a missingtypeof window !== 'undefined'guard on a window listener.live-chat: correctedcustom-elements.json'smembersarray (missingtheme/configaccessors, and an inaccurate description).sports-domain: corrected a stale code comment that seeded a documentation error in two Missions widgets.
Three fixes (
favorites,game-details,filters-settings) change real rendering and are flagged indocs/guide/integration-caveats.mdas needing human visual verification before release.
0.26.0
Stop publishing source maps and package source.
dist/*.js.mapwas emitted by the shared Vite preset and swept into every tarball byfiles: ["dist/", "src/"], with the original source inlined insourcesContent.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 withsourcemap: falseandfilesno longer carriessrc/.@gomagaming/corekeeps the two paths its export map actually points at:src/styles/main.cssandsrc/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-radioprop (JScardRadio) that renders a single-select radio affordance in each bet card's header. An empty circle by default, filled withhighlightPrimarywhen that card is the panel's selected bet. It is a dedicated flag, independent ofcard-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-readyevent. The host can finally tell when a session is usableA 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 theuserId, then the CE-login exchange fetches aceSessionand binds the WAMP socket, and only then does the place-bet payload carry theusernamethe 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-readycloses that gap, as the counterpart to the existinggoma:session-expired:jswindow.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:v1envelope is public, butstorageevents 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
usernameand 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
loginWithCeSessionanswers{ username, sessionToken, message }; a refused one answers a single-key payload, which is still truthy, so the guard let it through. That wrotestatus: 'valid'with a nullusernameand a nullsessionToken: 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 theuserIdthe REST probe had already established, so the bet went out with no identity at all and came back401.idlenow only tears the identity down when the host has actually dropped itssessionId, 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/403from 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
InvalidSessionmarker before condemning the session, matching the production sportsbook'shandleSessionExpiration. Anything else fails that one request and leaves the session alone. The session probe (GET /v1/player/session/player) and the balance stream'sSESSION_EXPIRATIONevent remain the authorities on whether the player is still signed in.A rejection is scoped to the credential it names.
markInvalid()now records whichsessionIdfailed, and the logout +goma:session-expiredonly 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 late401from the old one killed the session that had just replaced it. Requests can't be recalled: a/balancecall, 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.sessionIdused??, so an empty string overwrote a real id: hosts feeding a pre-resolved identity (userId+sessionToken, no rawsessionId) wiped the id every other widget had recorded, after whichisValidForcould never match and the CE-session exchange re-ran indefinitely.usernamehad the same flaw: only the CE-login exchange learns it (the REST probe never returns one), so any other committer nulled it, andusernamethen silently vanished from every subsequent place-bet payload. Both are preserved now; a real logout still clears them viaclear().A spent rejection left over from a previous
sessionIdis 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
401used to paint the literal stringHTTP 401into the betslip panel: the transport fallback, because the provider sends no body on a gateway rejection and HTTP/2 carries nostatusText. 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
sessionIdis 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_warningmessage key are removed: leaving the UI presenting a signed-in player who couldn't do anything was the worst of both. A place-bet401/403also renders no error text.goma:bet-failedstill carries the fullstatus/bodyfor telemetry.goma:session-expiredis the session-death contract. Every authoritative rejection funnels throughuseSharedSession().markInvalid(reason)and reaches the host: place-bet401/403, favourites401/403, the session probe, the CE-login exchange, and the SSE expiry that already fired it.detail.reasonnames 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:
- 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-expiredand show your own message. - Place-bet errors no longer include session failures. A
401/403writes nothing to the panel. If you were reading the rendered error text, switch to thegoma:bet-failedevent, which still carriesstatusandbody.
jslet 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.invalidationIdis 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:v1envelope 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 firstsessionIdof 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 normalsessionMaxAgeMsreuse window resumes.requireSessionnow gates on a usable session, not just a non-empty string. A stalesessionIdrestored from a host cookie satisfied the old check, so Place Bet stayed enabled and the submit went out to a guaranteed401. The widget can't clear the host-ownedsessionIdprop, 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
playerApiBaseUrlleaves 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.- 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
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 emitsgoma:my-bet-selecton card tap. Sharedsports-domainadditions:subscribeToEventPartScores+parseEventPartScore(per-event live-score feed),useMyBetsScoresLive, areloadTokenforce-reload hook inuseMyBets, aneventResultfield on mapped legs, and a card-tapselectemit onMyBetsCard/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-betsas a dependency and no longer run their ownuseMyBets()feed (the widget owns it).