Skip to content

<goma-missions-hub> ​

The Missions hub: a top-level Challenges / My Challenges tab strip over two sections. A joinable Challenges catalog (default view) and My Challenges, the player's own mission cards behind an Active / Pending / Finished sub-tab strip, with per-tab empty states whose illustrations are host-injectable. Tapping a card emits goma:mission-select so a sibling <goma-mission-details> can follow it: the same cooperation pattern as events-horizontal ↔ game-details.

Element tag ​

html
<goma-missions-hub></goma-missions-hub>

Installation ​

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

See Installation for the one-time .npmrc token setup.

Configuration ​

Set config for the runtime basics (shared by every widget) plus the dataSource: 'rpc' keys this widget reads:

KeyTypeRequiredDescription
localestringNoi18n locale (en / fr / pt bundled). Default 'en'.
themeobjectNo--goma-* token overrides.
messagesobjectNoPer-locale i18n overrides merged over the bundled copy.
apiBaseUrlstringWhen dataSource: 'rpc'Base URL for the EveryMatrix Challenges REST API.
domainIdnumberWhen dataSource: 'rpc'EM domain id.
playerSessionstringWhen dataSource: 'rpc'Player session token: authenticates every Challenges call.
userIdstringOnly for the Finished tabEM internal user id. Without it, PlayerHistory (the Finished feed) is skipped and stays empty; Active/Pending and the catalog are unaffected.

Data sources ​

Two modes, selected by the dataSource property:

  • 'prop' (default): host-fed. Provide the My Challenges mission list via the missions property (array, or the missions HTML attribute as a JSON string) and the Challenges catalog via the catalog property (same array-or-JSON-string handling).
  • 'rpc', the widget fetches the EveryMatrix Challenges REST API itself (createChallengeApi, plain fetch, no WAMP) via four independent feeds, one request each, and only when that section is shown: the Challenges catalog ← DisplayableWidgetChallenges (challenges the player is eligible to trigger but hasn't yet); My Challenges > Active ← GetChallengesInfo with IsFutureAvailable: false; Pending ← GetChallengesInfo with IsFutureAvailable: true (the call decides the sub-tab, not the progress status); Finished ← PlayerHistory (needs userId in config, see Configuration; without it no call is made and the tab is empty). A mount makes one call, and a tab switch makes one call for the tab it reveals. A failure only affects its own feed. Fetch-only, no live push. A showTabs: false instance is pinned to one view, so it never calls the other section's feeds and two instances split across challenges / my-challenges don't double the request load against the same session.

Assumed Mission shape ​

The backend does not expose a first-class missions read model for 'prop' mode, so host-fed data is shaped to this assumed contract (from useMissions.js):

js
{
  id, platform: 'casino' | 'sports', title, imageUrl,
  status: null | 'new' | 'suspended' | 'unavailable' | 'forfeit' | 'expired', statusLabel,
  totalLevels, reward, termsUrl,
  tab: 'active' | 'pending' | 'finished',       // which My Challenges sub-tab
  joinState: 'active' | 'not-joined' | 'pending' | 'finished' | 'forfeited',
  cta,                                    // optional explicit CTA override
  currentLevel, levelCount, progress,     // in-progress badge + bar
  endsAt,                                 // ms epoch → live "ends in" countdown
  unlockText, description,                // not-joined card's gold description line
  minDeposit,                             // "Min. deposit to start:" line when the challenge carries a deposit trigger
  awaitingNote,                           // pending card's note (falls back to bundled copy)
  levels: [{ level, state, progress, reward, requirement, completedAt }],
  totalWon,                               // finished tab only
  challengeId, internalUserId,            // carried through to goma:mission-select / goma:mission-cta when present
}

Props / attributes ​

PropertyAttributeTypeDefaultDescription
viewview'challenges'|'my-challenges''challenges'Which top-level section is shown; updated when the Challenges/My Challenges strip is tapped.
showTabsshow-tabsbooleantrueRenders the Challenges/My Challenges strip. false = single-section layout: only the view section renders, no strip.
catalogcatalogMission[][]The Challenges-tab catalog list.
missionsmissionsMission[][]The My Challenges mission list.
activeTabactive-tab'active'|'pending'|'finished''active'Initial My Challenges sub-tab; updated when a pill is tapped.
emptyStateImagesempty-state-images{ active?, pending?, finished?, challenges? }{ active: '', pending: '', finished: '' }Per-tab empty-state illustration URLs. active / pending / finished are the My Challenges sub-tabs; the optional challenges key illustrates the Challenges catalog empty state (unset = text-only card).
offersUrloffers-urlstring''Optional href hint carried in the empty-state CTA event.
dataSourcedata-source'prop'|'rpc''prop''prop' = host-fed; 'rpc' = fetch EM Challenges (needs config, see Configuration).
colorModecolor-mode'dark'|'light''dark'Toggles the root dark class so the widget follows the host app's light/dark theme.
stickyTopsticky-topnumber0Sticky offset (px) for the Challenges / My Challenges strip. The strip pins to the top of the host's scroll container as the list scrolls; pass the height of any fixed host chrome (e.g. a topbar) so it parks just below it. 0 = flush to the scroll top.

Appearance (styling object) ​

appearance carries structure only: radii, border widths, pill typography and size. These are mode-agnostic (identical in light and dark), so each key is a single value that maps to one --goma-* CSS custom property, written as an inline style on the widget's host element. Set it via el.appearance = {...} (the JS property) or the appearance="..." HTML attribute (a JSON string, parsed on write). Unset keys leave the current default untouched: an empty {} (the default) reproduces the widget's current look exactly.

Colours are not in appearance. Every mission colour (pill fills and text, CTA button colours, level/reward/requirement/tag colours) moved to config.theme because colours need light/dark pairs: set them via el.theme = {...}. See the Missions section of the Theming guide for the full colour-token list.

<goma-missions-hub> and <goma-mission-details> share the same structure map, so you can pass a host's single appearance object to both: each widget applies the keys that map onto surfaces it renders. levelCardRadius is carried in the shared map but is a no-op on the hub: it styles <goma-mission-details>'s Levels-tab cards (see its docs).

KeyCSS variableAffects
cardRadius--goma-radiusCardMission-card corner radius. It applies to all hub cards: Active / Pending challenger cards and the Finished tab's history cards.
buttonRadius--goma-radiusButtonCTA button corner radius.
pillRadius--goma-radiusPillTab-pill corner radius.
pillActiveBorderWidth--goma-pillActiveBorderWidthActive pill ring width: set to '0' to render a solid fill instead of a ring.
pillFontWeight--goma-pillFontWeightInactive tab-pill font weight.
pillActiveFontWeight--goma-pillActiveFontWeightActive tab-pill font weight.
pillHeight--goma-pillHeightTab-pill height.
pillPaddingX--goma-pillPaddingXTab-pill horizontal padding.
gamesColumns--goma-gamesColumnsColumns of the Games tab grid on <goma-mission-details> (tiles and skeletons). Default 3.
gameTileRatio--goma-gameTileRatioAspect ratio of a game tile, for example 168 / 146. Default 1 / 1.
detailTabsFullWidth--goma-detailTabsFullWidthtrue removes the 16px side padding of the detail tab strip. Default false.
detailTabIndicatorHeight--goma-detailTabIndicatorHeightHeight of the ACTIVE detail tab underline only. Inactive tabs draw the 1px missionDetailTabTrack line. Default 2px.
topTabIndicatorHeight--goma-topTabIndicatorHeightHeight of the bottom line of both top tabs (Challenges and My Challenges), active and inactive. Default 2px.
pillsBarPaddingBottom--goma-pillsBarPaddingBottomSpace under the pills inside the pill row strip (the strip is the missionPillsBarBg background). Default 0px, which keeps the layout without a strip.
fontFamily--goma-fontFamilyFont of the widget text. Default the widget font. inherit takes the host font.

Mode-agnostic

appearance layers on top of whichever dataSource is active ('prop' or 'rpc'). It only ever touches presentation, never data fetching.

Quit-challenge modal is host-driven

The quit-challenge confirmation modal (on <goma-mission-details>) is rendered by the host, not the widget. <goma-mission-details> only emits goma:mission-optout once the host confirms the quit. Neither appearance nor theme has any effect on that modal; style it yourself.

Worked example: a light-theme host. Structure goes on appearance, colours go on theme:

js
// structure (mode-agnostic single values)
el.appearance = {
  cardRadius: '0.75rem',
  pillRadius: '9999px',
  pillActiveBorderWidth: '0',
}

// colours (need light/dark parity → config.theme)
el.theme = {
  pills: '#f0f0f2',
  pillText: '#252634',
  pillActiveBg: '#ff6600',
  pillActiveText: '#ffffff',
  missionButtonBg: '#40b840',
  missionButtonText: '#ffffff',
  missionButtonBorderColor: '#40b840',
}

Events ​

All events are composed + bubbling (cross the Shadow DOM boundary).

EventdetailWhen
goma:tab-change{ view }The top-level Challenges/My Challenges strip switched section.
goma:mission-select{ missionId, challengeId, mission }A mission card was tapped. challengeId is the stable EM challenge id (falls back to missionId for host-fed missions that carry no challengeId): route a deep-link details page on this, since <goma-mission-details>'s by-id resolution filters on ChallengeId.
goma:mission-cta{ missionId, challengeId, internalUserId, action }A card's CTA was pressed. action is start / resume / pause / view for enrolled cards; for a catalog card it comes from the challenge's enabled triggers (priority Deposit > Game Launch > Claim) → deposit / launch / claim. missionId is the per-enrollment progress id; challengeId is the stable challenge id; internalUserId is the EM-internal user id the write endpoints need. All three are null/absent for host-fed missions that don't carry them.
goma:missions-count{ view, status, counts }The mission counts settled or changed: see Reporting counts to the host. Fires once the feed settles and again on every change (a CTA mutation, refresh(), a host-fed prop write); an identical payload never re-fires.
goma:missions-offers{ tab, offersUrl }The Active tab's empty-state CTA ("See current offers") was pressed. In the bundled copy this is the only empty state that actually reaches this event: Pending's empty state renders no CTA, the Finished tab's CTA switches the widget to Active locally instead of emitting, and the Challenges catalog's empty state renders no CTA either.
goma:retry{}The render-error banner's Retry button is pressed.
ready{}Mounted and rendered.
goma:error{ message, code, component? }Fires on 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. The connection-layer path (code: 'API_INIT_ERROR') does not exist here: Missions is host-fed / rpc-fetched over REST-shaped data, not a WAMP sports-API connection.

Reporting counts to the host ​

A host that hides the widget's own tab strip (showTabs: false) draws its own chrome, and then has no way to know how many missions the widget is holding, which is what a tab badge needs. goma:missions-count reports it:

js
hub.addEventListener('goma:missions-count', (e) => {
  const { view, status, counts } = e.detail
  // counts = { active, pending, finished, total, catalog }
  // In rpc mode `total` is always null: count it yourself (see below).
  if (counts.total != null) badge.value = counts.total
})
FieldMeaning
viewWhich section this instance renders: 'my-challenges' or 'challenges'. Lets one listener tell two instances apart.
status'ok' or 'error'. Tracks the My Challenges feed only; a catalog failure is warn-only in the source and leaves the rest intact.
counts.active / .pending / .finishedMissions in each My Challenges sub-tab.
counts.totalProp mode: active + pending + finished, everything the My Challenges view renders. In rpc mode: always null (see below).
counts.catalogOffers in the Challenges catalog.

In rpc mode total is always null

The widget fetches one request per tab. So it never holds all three My Challenges lists fresh at once. A sum would add one fresh list to older ones. An instant change (for example a forfeited progress) also removes an item before any fetch. So the widget does not send a mixed total. counts.catalog is one fresh request and stays correct. active, pending and finished are each from their own last fetch. A host that needs a total must count it itself, for example by reading all the feeds. In prop mode the host feeds the data, so the total stays.

null means unknown, not zero

In dataSource: 'rpc' each section is fetched only when it is shown, so a bucket that has not been fetched yet is reported as null (catalog stays null until the catalog has loaded). Treat null as "no information" and leave your badge alone: a 0 would be indistinguishable from an empty feed. This matters when two instances sit on one page: the event is composed + bubbling, so a single host listener receives both, and blindly reading counts.total from the catalog instance would clobber the real number.

The event fires once the feed settles and again whenever the counts change, so a badge stays correct across refresh() and CTA mutations without polling. It cannot report before the widget is mounted: a host that lazily mounts the widget (rendering only the active tab's content, say) gets nothing until that tab is first opened, and needs its own source for the initial paint.

Instant updates and confirmation ​

The EverMatrix (EM) read feeds lag a successful write by about 1.1 s. A refetch right after a write still returns the old data. So the widget can show the new state at once, and then confirm it with fresh data.

Methods that create an instant change. The host calls them right after the write returns Success: true (or after an SSE message):

  • markClaimed(challengeId): hides the claimed card in the catalog.
  • setProgressStatus(progressId, status): shows 'in-progress', 'paused' or 'forfeited'.
  • setChallengeAvailable(challengeId, available): shows a challenge as Unavailable or normal again.

How long it lives. An instant change lives for 10 s at most.

When it ends early. It ends as soon as fetched data agrees with it. A stale refetch in between does not undo it.

What happens at the end. If the 10 s pass and the data never agreed, the widget does NOT fall back to its cached data. The instant change stays in force and the visible tab refetches ONE more time. When that refetch settles (it worked or it failed), the instant change ends and the fetched data decides. The card never shows the old state in between. Several changes that end together give one refetch, not many. A hard cap protects you: if the refetch has not settled 10 s after the 10 s, the instant change ends anyway. It also ends at once when the player or the config changes.

A refresh during a fetch. A refresh() that comes while a fetch is running is not answered by that older fetch, because it may have started before the write. The widget runs exactly one more fetch after it. Several refreshes in the meantime share that one extra fetch.

What the host must do. Call refresh() about 1.5 s after a write or an SSE message. That is just after the EM lag. Restart the 1.5 s timer on each new event, so a burst of events gives one refresh. Add a max wait: fire the refresh at the latest 4 s after the FIRST event of the burst, so a steady stream of events cannot delay it forever.

Timeline example.

TimeWhat happens
0 sThe player taps Pause. The host sends the write and calls setProgressStatus(id, 'paused'). The card shows Suspended at once.
1.5 sThe host calls refresh().
1.5 s, server agreesThe fetched item has the Paused status. The instant change ends. Done.
1.5 s, server does not agree yetThe card keeps Suspended.
10 sThe widget refetches once. The card keeps Suspended while it runs.
When that refetch settles (20 s at the latest)The instant change ends. The fetched data decides the card.

List size ​

Each tab (Challenges, Active, Pending, Finished) loads one page of 20 items (PageSize 20). There is no paging and no "load more" control, by design (owner ruling 2026-10-08).

Methods ​

MethodDescription
refresh()Re-runs the data source in dataSource: 'rpc' mode (re-fetches the feed of the section currently shown: one request). Call it after a successful host-side CTA mutation so the list reflects the new state without a reload. No-op in 'prop' mode: the host stays the source of truth there.
markClaimed(challengeId)Hides the catalog (Challenges tab) card whose challengeId equals the argument (compared as strings). Call it right after the host's claim write returns Success: true, because EverMatrix read feeds lag a write by about 1.1 s and a refetch would still list the challenge. The override ends when a fetched catalog no longer lists the challenge, or after 10 s, whichever comes first. A stale refetch in between does not bring the card back.
setProgressStatus(progressId, status)Shows every card or details view whose mission id (the EM progress id, compared as strings) equals progressId as if EM had returned Status InProgress ('in-progress', 1), Paused ('paused', 7) or Forfeited ('forfeited', 5). The CTA, the suspended tag and, after an opt-in from Started (0), the current level, progress and unlocked ladder all follow. Call it right after ChangeProgressStatus returns Success: true. The override ends when a fetched item carries the same Status, or after 10 s, whichever comes first. A stale refetch in between does not undo it. A 'forfeited' progress leaves the Active and Pending lists (the Finished tab is history data, fetched separately), and the override also ends when the progress is gone from the fetched feeds. In 'prop' mode only progressStatus, the suspended tag and the CTA change: the level row stays as the host fed it.
setChallengeAvailable(challengeId, available)false: every item whose ChallengeId equals the argument (compared as strings) gets ChallengeStatus Paused (2), as when the operator disables the challenge in UBO: the Unavailable tag, the greyed card and the "View Details" CTA. It covers the catalog, My Challenges and the details view. true: every such item gets ChallengeStatus Enabled (1), even when a refetch right after the SSE still returns Paused (2). Each override ends when the fetched items agree (all unavailable for false, all Enabled for true), when the challenge is gone from the fetched feeds, or after 10 s, whichever comes first. In 'prop' mode false sets the status to unavailable, the label and a fed cta; true clears an unavailable status.

markClaimed, setProgressStatus and setChallengeAvailable exist on <goma-missions-hub> and <goma-mission-details> and write to one shared store, so a call through either element shows in both when they share the same @gomagaming/sports-domain copy. A CDN page needs exactly one import map entry for it: see Why an import map is needed at all.

ChallengeStatus rule (challenge level, on catalog and progress items). The operator sets ChallengeStatus in UBO. Any value other than Enabled (1), meaning Draft (0), Paused (2), Closed (3) or Deleted (4), or IsAvailable === false, makes the challenge Unavailable. Unavailable wins over every player progress Status: tag "Unavailable", greyed and inert card, CTA "View Details" only, no progress bar, no current-level row, levels greyed, and no action in the details footer. A player pause is a different thing: only Status Paused (7) on an Enabled challenge renders as Suspended, with the Resume CTA.

Integrates with ​

Emits goma:mission-select on every card tap: no follow* gate, it always fires. Commonly paired with a sibling <goma-mission-details>, which follows it via its own followMissionSelect prop (see that widget's page for the default and opt-out). See Cross-widget wiring for the full cooperation graph.

This widget reads and writes no shared-storage envelope: missions state is either host-fed ('prop') or owned by this widget's own REST fetch ('rpc'); nothing is shared with other widgets via localStorage.

Empty / loading / error states ​

  • Loading, while dataSource: 'rpc' has an in-flight fetch and the active section has no items yet, three pulsing skeleton cards render in place of the list (both the Challenges catalog and the My Challenges sub-tabs use the same skeleton).
  • Empty: a per-section empty state renders once the feed answers with zero items: the Challenges catalog shows missions_empty_challenges_* copy with no CTA and an optional host-injected emptyStateImages.challenges illustration (none by default); each My Challenges sub-tab shows its own copy (missions_empty_active_* / _pending_* / _finished_*) plus an optional host-injected emptyStateImages[tab] illustration. Only Active and Finished render a CTA button (Pending has none); tapping it on Active emits goma:missions-offers, while on Finished it switches the widget to the Active sub-tab locally instead (no event).
  • RPC fetch error: in dataSource: 'rpc' mode, if the feed of the shown My Challenges sub-tab fails and its list is still empty, an inline message ("Couldn't load challenges. Please try again.", key missions_list_error) plus a Retry button render in place of the list; the button re-fetches that sub-tab only (no event emitted). A DisplayableWidgetChallenges (catalog) failure degrades silently to an empty catalog: it does not show this error state.
  • Render error: an uncaught render error inside the widget tree shows a themed error banner with the raw error message, a dismiss (×), and a Retry button (emits goma:retry); the banner auto-clears after 8 seconds.
  • Finished-tab history cards start collapsed: every card in the Finished tab renders banner + header only and expands on a header tap. The tab can hold a long history, so an expanded-by-default wall of level timelines is avoided. (Previously only expired cards collapsed; now all do.)

Theming ​

Reads --goma-* tokens. Each element reads the token named like its design variable, plus a few role tokens: see Role tokens and layout settings below, which overrides the list that follows where they differ. Backgrounds: --goma-backgroundPrimary (section), --goma-backgroundSecondary (card banner), --goma-backgroundCards (mission card), --goma-backgroundBorder (empty-state border), --goma-separatorLine (progress-bar track, card border), --goma-separatorLineSecondary ("unavailable" / "expired" status pills), --goma-pillBg (resting Active/Pending/Finished pill fill, default --goma-pills). Text: --goma-textHeadlinePrimary, --goma-textPrimary, --goma-textSecondary. Icons: --goma-iconPrimary. Highlights: --goma-highlightPrimary (selected top-level tab; "new" status pill), --goma-highlightPrimaryContrast (focus ring), --goma-highlightSecondary (terms link; progress-bar fill end), --goma-highlightTertiary (progress-bar fill start; "suspended" status pill; "finished" status pill, filled green with white text), --goma-pillActiveBg / --goma-pillActiveText (selected sub-tab pill fill), --goma-pillActiveRing (what the selected pill's ring is painted with, any CSS <image>, so the ring can be a gradient rather than a flat colour; unset it paints a flat sheet of --goma-pillActiveBorderColor and looks exactly like a solid border. A host can send a conic gradient built from var(--goma-separatorLine) and var(--goma-highlightPrimary), so it holds no hex. See Giving the active tab pill a gradient ring): see Theming for the pill colour tokens. Buttons: --goma-allWhite / --goma-allDark (status-pill fill and/or text across the "new" / "ends-in" / "forfeit" / "expired" / "suspended" / "finished" badge states; progress-bar center label), --goma-missionButtonBg / --goma-missionButtonText (solid CTA fill/text for join actions, start / deposit / claim / launch), --goma-missionButtonBorderColor (outline CTA border/text for resume / view actions). Alerts: --goma-alertError (forfeited-level marker, render-error banner), --goma-myTicketsLost ("forfeit" status pill). Mission-card colours: --goma-missionLevelBadgeBg / --goma-missionLevelBadgeBorderColor / --goma-missionLevelBadgeText (on-card progress bar's current-level node fill/border/number colour), --goma-missionUnlockText (not-joined unlock/description line), --goma-missionValueText (value text, total levels, level requirement, reward, min deposit), --goma-missionTagBg / --goma-missionTagBorderColor (platform pill fill/border, Casino / Sports), --goma-missionHistoryWonColor (Finished-tab history: completed-level circle fill, reward text, "Completed" status text). See Theming for the full token reference.

Role tokens and layout settings ​

The rule. Every colored element reads the --goma-<name> token whose name is the design color variable on that layer. The host maps its own palette by identity: app <name> goes into --goma-<name>. A host never changes its app palette to fit the widget. Each color token has a -dark twin (--goma-<name>-dark) and the widget paints it as text-<name> dark:text-<name>-dark (or bg- or border-).

Role tokens. Where designs use different variables for the same element, the widget reads one role token and each app fills it from its own variable. With no value set, a role token keeps the color the element had before (the Default column), so an app that sends nothing sees no change from the role tokens. The exceptions are the five private tokens with a new default and the elements listed in "Elements that changed token" below: those follow the design variable, so a host whose palette gives them other values sees a change. A role token that already existed gets a -dark twin that defaults to its light token (var(--goma-<name>)), so a host that sets only the light value still paints both modes.

Role tokenElementsDefaultWhen to set it
pillBg (new)Resting sub-tab pill fillvar(--goma-pills)Send it when the design draws this element with a variable other than the default.
pillActiveTextSelected sub-tab pill label#ffffffSend it when the design draws this element with a variable other than the default.
pillActiveBgSelected sub-tab pill fill#2a2d60Send it when the design draws this element with a variable other than the default.
missionButtonBgSolid CTA fill (hub card, details footer)var(--goma-allWhite)Send it when the design draws this element with a variable other than the default.
missionButtonTextSolid CTA labelvar(--goma-allDark)Send it when the design draws this element with a variable other than the default.
missionButtonBorderColorOutline CTA border, and the Games "Load more" border and labelvar(--goma-allWhite)Send it when the design draws this element with a variable other than the default.
missionCtaOutlineText (new)Outline CTA labelvar(--goma-missionButtonBorderColor)Send it when the design draws this element with a variable other than the default.
missionDetailTabIndicator (new)Active underline of the detail tabsvar(--goma-textPrimary)Send it when the design draws this element with a variable other than the default.
missionDetailTabTrack (new)Inactive track: the 1px line under the detail tab strip. Inactive tabs draw no line of their ownvar(--goma-separatorLine)Send transparent when the design draws no line for inactive tabs.
missionTopTabTrack (new)The 1px line under the top tab strip (Challenges and My Challenges)var(--goma-separatorLine)Send the variable the design uses for this line.
missionTopTabInactiveLine (new)Bottom line of an inactive top tab. The default is transparent, so the strip line shows throughtransparentSend the variable the design uses for this line.
missionUnlockTextUnlock or description line on a not-joined cardvar(--goma-highlightTertiary)Send it when the design draws this element with a variable other than the default.
missionValueTextValue text on a card (min deposit, levels, requirement, reward)var(--goma-highlightTertiary)Send it when the design draws this element with a variable other than the default.
missionLevelBadgeTextCurrent level number on the card progress barvar(--goma-textPrimary)Send it when the design draws this element with a variable other than the default.
missionTagBorderColorBorder of the Casino or Sports pillvar(--goma-highlightTertiary)Send it when the design draws this element with a variable other than the default.
missionHistoryWonColorFinished tab: TOTAL WON border and value, completed level circle, reward and statusvar(--goma-highlightTertiary)Send it when the design draws this element with a variable other than the default.
missionLevelActiveBorderStart / missionLevelActiveBorderEndTwo stops of the active level card gradient borderhighlightTertiary / highlightSecondarySend two variables for a two-color border, or the same variable twice for one color.
missionRulesTermsLink (new)"Read full terms" link on the Rules tabvar(--goma-highlightTertiary)Send it when the design draws this element with a variable other than the default.
missionProgressFillStart (new)From stop of the progress fill gradient (card progress bar, level row bar). The to stop stays highlightSecondaryvar(--goma-highlightTertiary)Send it when the design draws this element with a variable other than the default.
missionTermsLink (new)Terms & Conditions link on the challenge card, the header and the finished cardvar(--goma-highlightSecondary)Send it when the design draws this element with a variable other than the default.
missionHeaderUnlockText (new)Not-joined unlock prompt in the challenge headervar(--goma-highlightPrimary)Send it when the design draws this element with a variable other than the default.
missionHubBg (new)Page surface behind the hub (below the tab band)var(--goma-backgroundPrimary)Send it when the design draws this element with a variable other than the default.
missionPillsBarBg (new)Full-width strip behind the Active / Pending / Finished pill rowtransparentSend it when the design draws a full-width strip behind the pill row. With no value, no strip shows.

New -dark twins. pillText, pillActiveBorderColor, missionLevelBadgeBg, missionLevelBadgeBorderColor and missionLevelCompletedBorderColor keep their defaults. Each has a new -dark twin that defaults to the light token, so a host that sets only the light token changes dark mode too.

Platform pill gradient. Two new private tokens paint the Casino or Sports pill.

Private tokenElementDefault (light)Was
missionTagBgStart / missionTagBgEnd (new)Casino or Sports pill gradient, left to rightvar(--goma-backgroundSecondary) / var(--goma-backgroundSecondary) (a flat pill)(flat missionTagBg, default backgroundSecondary)

missionTagBg has no default any more. The pill paints the missionTagBgStart to missionTagBgEnd gradient. Both ends default to backgroundSecondary, which is the flat fill the pill had before, so a host that sends nothing sees the same pill. A host that wants a gradient sends missionTagBgStart (for example backgroundPrimary) and missionTagBgEnd (for example backgroundSecondary). A host that still sets missionTagBg (or missionTagBg-dark) gets that flat color instead, so existing hosts keep working.

Layout settings. Six appearance keys cover small per-design layout differences. Each default equals the previous look. A boolean is written to the CSS variable as 1 or 0.

KeyCSS variableDefaultWhen to set it
gamesColumns--goma-gamesColumns3Send 2 for a two-column tile grid.
gameTileRatio--goma-gameTileRatio1 / 1Send the tile aspect ratio the design draws, for example 168 / 146.
detailTabsFullWidth--goma-detailTabsFullWidthfalseSend true when the design runs the tab strip edge to edge.
detailTabIndicatorHeight--goma-detailTabIndicatorHeight2pxSend the underline height the design draws, for example 6px.
topTabIndicatorHeight--goma-topTabIndicatorHeight2pxSend the bottom line height the design draws.
pillsBarPaddingBottom--goma-pillsBarPaddingBottom0pxSend 16px when the design leaves space under the pill row.
fontFamily--goma-fontFamilythe widget fontSend inherit to use the host font.

gamesColumns and gameTileRatio style the Games tab (tiles and loading skeletons). detailTabsFullWidth: true removes the 16px side padding of the detail tab strip. detailTabIndicatorHeight sets the active underline height. topTabIndicatorHeight sets the bottom line height of both top tabs. The tab label does not move when either height changes. fontFamily: 'inherit' makes the widget use the host font.

Accepted values. A value the browser cannot parse does not fall back to the widget default. The browser drops the declaration and uses ITS default for that CSS property (for example no grid columns, or the inherited font). Send valid values only.

KeyAccepted value
gamesColumnsA positive integer, for example 2.
gameTileRatioA CSS aspect-ratio, for example 1 / 1 or 168 / 146. Not 16:9.
detailTabsFullWidthA boolean, true or false. Not the string 'false'.
detailTabIndicatorHeightA CSS length, for example 2px.
topTabIndicatorHeightA CSS length, for example 2px.
pillsBarPaddingBottomA CSS length, for example 16px.
fontFamilyA CSS font-family list, or inherit.

A --goma-fontFamily that the host sets through theme is kept when appearance does not set fontFamily. The older radius, size and spacing keys take a CSS length. The font weight keys take a CSS font weight.

Elements that changed token. The table lists the element, the old token and the new token. Both the light class and the dark: class changed.

ComponentElementOldNew
Hub pageSticky top tab band fillbackgroundCardsbackgroundTertiary
Hub pageChallenges and My Challenges headingstextHeadlinePrimarytextPrimary
Top tabsStrip underline, inactive underlineseparatorLine, transparentmissionTopTabTrack (default separatorLine), missionTopTabInactiveLine (default transparent)
Top tabsInactive labeltextSecondarytextPrimary
Sub-tab pillsResting pill fillpillspillBg (default pills)
Details pageBack label and chevrontextHeadlinePrimarytextPrimary
Pill rowStrip behind the pillsnonemissionPillsBarBg (default transparent)
Hub pagePage surfacebackgroundPrimarymissionHubBg (default backgroundPrimary)
Challenger card, header, finished cardTerms & Conditions linkhighlightSecondarymissionTermsLink (default highlightSecondary)
Challenger headerNot-joined unlock prompthighlightPrimarymissionHeaderUnlockText (default highlightPrimary)
Detail tabsInactive underline, inactive labeltransparent, textSecondaryseparatorLine, textPrimary
Challenger cardTitletextHeadlinePrimarytextPrimary
Card progress barTrackseparatorLinescroll
Card progress barCentre labelallDarkhighlightSecondaryContrast
Card progress barNext level node fillseparatorLinebackgroundBorder
Platform pillLabelallWhitetextPrimary
Status tag, ends-inFill, label, borderallWhite, allDark, allWhitehighlightPrimaryContrast, highlightSecondaryContrast, shadow
Status tag, suspendedLabelallDarkhighlightSecondaryContrast
Status tag, finishedFill, labelhighlightTertiary, allWhitehighlightSecondary, highlightSecondaryContrast
Status tag, forfeitFill, labelalertError, allWhitemyTicketsLost, highlightPrimaryContrast
Status tag, expiredLabelallWhitehighlightPrimaryContrast
Status tag, newLabel, borderallWhite, allWhitehighlightPrimaryContrast, shadow
Challenge headerTitletextHeadlinePrimarytextPrimary
Empty stateTitle; card variant border; text-only filltextHeadlinePrimary; separatorLine; backgroundCardstextPrimary; backgroundBorder; backgroundTertiary
Empty stateText-only CTA linkhighlightSecondaryhighlightTertiary
Rules panelContribution type and rule valuestextSecondarytextPrimary
Events panelContribution percent below 100% / at 100% or morehighlightPrimary / highlightTertiarymissionEventsPercent (default highlightPrimary) / missionEventsPercentComplete (default highlightTertiary)
History cardOuter border; titleseparatorLine; textHeadlinePrimarybackgroundBorder; textPrimary
Level cardActive card fill; "Completed" label; progress trackbackgroundCards; highlightSecondary; separatorLinebackgroundSecondary; missionLevelCompletedText (default highlightSecondary); scroll

i18n / message overrides ​

Bundled keys this widget's own components reference (en defaults): missions_challenges_tab ("Challenges"), missions_my_challenges_tab ("My Challenges"), missions_active_tab / _pending_tab / _finished_tab, missions_challenges_title / _subtitle, missions_active_title / _pending_title / _finished_title (+ _subtitle variants), missions_total_levels, missions_get, missions_ends_in, missions_level_n, missions_awaiting_note, missions_min_deposit ("Min. deposit to start:". The challenger card's deposit-trigger line, shown when a mission carries a minDeposit), missions_status_suspended, missions_cta_start / _deposit / _resume / _view / _pause / _discover / _claim / _launch, terms_conditions, missions_empty_active_title / _subtitle / _cta, missions_empty_pending_title / _subtitle, missions_empty_finished_title / _subtitle / _cta, missions_empty_challenges_title / _subtitle, missions_platform_casino / _sports (the platform pill on every card), and these keys on the Finished tab's history card: missions_levels_count, missions_total_won, missions_level, missions_completed, missions_forfeited. Plus retry (the error banner's Retry button). Override any of these via config.messages.

Embedding examples ​

Vanilla / CDN ​

html
<goma-missions-hub id="hub"></goma-missions-hub>
<script type="module">
  import '@gomagaming/missions-hub'
  const hub = document.getElementById('hub')
  hub.config = { locale: 'en' }
  hub.missions = [/* Mission[] */]
  hub.catalog = [/* Mission[] */]
  hub.emptyStateImages = {
    active: 'https://…/goal.svg',
    pending: 'https://…/stopwatch.svg',
    finished: 'https://…/star.svg',
    challenges: 'https://…/lightning.svg', // optional: catalog empty state
  }
  hub.addEventListener('goma:mission-select', (e) => console.log(e.detail.missionId))
</script>

React ​

jsx
import '@gomagaming/missions-hub'

<goma-missions-hub
  ref={(el) => {
    if (!el) return
    el.config = { locale: 'en' }
    el.missions = missions
    el.catalog = catalog
  }}
  onGoma:mission-select={(e) => openDetail(e.detail.missionId)}
/>

Vue ​

vue
<script setup>
import '@gomagaming/missions-hub'
import { ref } from 'vue'
const missions = ref([/* Mission[] */])
const catalog = ref([/* Mission[] */])
</script>

<template>
  <goma-missions-hub
    :missions.prop="missions"
    :catalog.prop="catalog"
    @goma:mission-select="onSelect"
  />
</template>

Accessibility ​

The top-level Challenges/My Challenges strip and the Active/Pending/Finished sub-strip are both role="tablist" with role="tab" buttons carrying aria-selected; keyboard focus follows normal tab order (no roving tabindex). Each mission card is role="button" with tabindex="0", aria-label set to the mission title, and Enter/Space trigger the same select as a click; a card whose status === 'unavailable' drops role, tabindex, and gets aria-disabled="true" instead, going fully inert to whole-card selection while its CTA button stays reachable. Focus rings use focus-visible throughout (tabs, cards, CTA buttons, empty-state CTA).

Integration Caveats ​

  • SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory. This widget is a direct target of an upcoming host integration, so this fix matters in particular.

  • Tokens/Theming: the docs incorrectly claimed the progress-bar's current-level node uses --goma-buttonActiveHoverTertiary; corrected to the tokens it actually uses (--goma-missionLevelBadgeBg/BorderColor/Text), and added several other real mission-card tokens that were missing from the Theming section.

  • Initialization: autobahn-browser is a required peer dependency even though this widget's rpc mode is plain REST; documented why (a transitive build-time artifact of @gomagaming/sports-domain, not something this widget itself needs).

  • 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.2
  • The Challenges widgets (<goma-missions-hub> and <goma-mission-details>) now read Figma color tokens, as a light class and a dark: class. New optional role tokens, each with a -dark twin: pillBg, missionCtaOutlineText, missionDetailTabIndicator, missionDetailTabTrack, missionTopTabTrack (default separatorLine), missionTopTabInactiveLine (default transparent), missionRulesTermsLink, missionProgressFillStart, missionEventsPercent, missionEventsPercentComplete, missionLevelCompletedText (default highlightSecondary), missionTermsLink, missionHeaderUnlockText, missionDetailTabActiveText, missionHubBg, missionDetailsBg, missionPillsBarBg, missionTagBgStart and missionTagBgEnd. New optional appearance keys: gamesColumns, gameTileRatio, detailTabsFullWidth, detailTabIndicatorHeight, topTabIndicatorHeight, pillsBarPaddingBottom and fontFamily. A fontFamily that the host sets through theme is no longer removed when appearance does not set it.

    New element methods on both elements, for an optimistic update right after a write: markClaimed, setProgressStatus and setChallengeAvailable. New @gomagaming/sports-domain exports: markClaimed, setProgressStatus, setChallengeAvailable, isClaimed, getStatusOverride, applyStatusToMission, applyAvailabilityToMission, isForfeitedOverride, filterClaimed and OVERRIDE_TTL_MS. An override belongs to one player session: a new player session drops all of them.

    Data fixes: one request per tab, a page size of 20, a Suspended or Unavailable badge on the details header, Quit hidden for a Started challenge, a games skeleton until the games of this challenge arrive, and the forfeited snapshot and lookup now use the ChallengeId. In rpc mode goma:missions-count now sends total: null, and active, pending and finished stay null until that sub-tab is loaded.

    No breaking changes: no prop, event or token was removed. Every new token and setting defaults to today's look, and the defaults of the existing private tokens are unchanged. Each of these five private tokens now has a -dark twin that defaults to its light value, so a host that sets only the light value still paints both modes: pillText, pillActiveBorderColor, missionLevelBadgeBg, missionLevelBadgeBorderColor and missionLevelCompletedBorderColor.

    About 40 elements now read a different existing palette token, so that they follow the Figma variable on that layer. A host whose palette gives those tokens different values sees those colors change. With the default palette and no new token, these colors change:

    • Inactive top tab label: #939dff to white (textPrimary).
    • Card progress track and level progress track: #2a2d60 to #393d83 (scroll).
    • Active level card fill: #1f2147 to #161732 (backgroundSecondary).
    • ColorTag ends-in and new border: white to #03061b (shadow).
    • ColorTag finished: green fill with white text to #404cff fill (highlightSecondary) with #03061b text (highlightSecondaryContrast).
    • ColorTag forfeit fill: #f9554d to #60111b (myTicketsLost).
    • Rules tab contribution text: #939dff to white (textPrimary).
    • Inactive detail tab label: #939dff to white (textPrimary).
    • Empty state text link: #404cff to #01b72e (highlightTertiary).
0.36.1
  • missions-hub: the Challenges catalog empty state can show an illustration. It rendered MissionsEmptyState with a hard-coded :image-url="null", so a host could illustrate the My Challenges empty states but not this one. It now reads an optional emptyStateImages.challenges URL. Unset keeps today's text-only card, so hosts that do not pass it see no change. The playground mock (MOCK_EMPTY_STATE_IMAGES in sports-domain) gains a challenges illustration so the playground shows it.

  • 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
  • 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.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
  • Missions: make locked and completed level cards readable, show a Forfeited tag once a mission is left, draw the lock and trophy from the design vectors, and let a host lift the detail CTA above its own bottom chrome

    The Levels tab told its three level states apart twice over: once through the card treatment (gradient border, solid border, no border) and again by muting the copy to textSecondary on everything that was not the active card. Locked cards then dimmed the whole card to 40% on top of the already-muted grey, which is why the text measured rgb(170,170,170) against a design that asks for white. Completed cards had the opposite half of the same bug: they inherited the muted typography but were never dimmed at all, so they rendered at full opacity.

    Typography is now identical in all three states and opacity alone separates them: active at full strength, completed at 60%, locked at 40%. Expect the completed card to shift less than the diff suggests: brighter text at 60% lands close to muted text at 100%, which is the intended result, not a fix that failed to apply.

    The lock glyph and the Total Won trophy were both hand-drawn approximations rather than the design vectors: the lock had rounded corners and no keyhole, the trophy a different silhouette entirely. Both are now the exported vectors, inheriting their colour through currentColor exactly as before.

    A forfeited mission kept counting down in the detail header. The banner tag was gated only on there being a countdown, and the clock keeps running on a challenge the player has left, so the header advertised time remaining beside a body that had already switched to the left-challenge state. Forfeiting now outranks the countdown and the banner shows a "Forfeited" tag instead.

    The "you've left this mission" panel painted itself with backgroundOdds: the odds-BUTTON colour, not a surface. Themes whose odds buttons are light rendered a white slab on a dark page, and because the panel's heading is textPrimary white, the heading disappeared into it entirely. It now paints the page background, matching the design, with its border, corner radius and padding unchanged.

    The selected tab pill's ring can now be painted with a gradient. It was built as a border, which only accepts a colour, so a host could choose the ring's colour and thickness but never make it a sweep, and the tab strip is styled differently by each app that embeds it, so that was not the widget's call to make. The ring is now a background layer and --goma-pillActiveRing sets what it is painted with, accepting any CSS <image>. Left unset it paints a flat sheet of --goma-pillActiveBorderColor, which renders exactly like the solid border it replaced, so no existing host moves. Repeat your first colour as the last stop on a conic gradient, or the 360°→0° wrap leaves a seam.

    This also removes a .tab-pill-active rule and a --goma-tabPillActive token that nothing referenced. The rule carried a hardcoded gradient no widget ever applied, and its .dark variant could not have matched under any circumstances, since dark mode swaps the --goma-* values rather than adding a .dark class.

    The mission-detail footer CTA now renders its label in uppercase, matching the design.

    The detail view's sticky footer CTA resolves bottom: 0 against the viewport, so a host page with its own fixed bottom chrome (a nav bar, a cookie bar) had the CTA scrolling underneath it, with no way for the widget to measure something outside its own shadow root. Two new tokens let the host declare it: --goma-stickyFooterOffset reserves the space and --goma-stickyFooterZIndex settles the stacking order. Neither has a default, so a host that sets neither sees no change.

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.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.25.1
  • Missions hub: refresh the challenges feeds on a tab switch.

    <goma-missions-hub> now re-pulls when the player switches the top view (Challenges ↔ My Challenges) or a My-Challenges sub-tab (Active / Pending / Finished), instead of only on mount or an explicit refresh(). A challenge claimed elsewhere lands on My Challenges > Active immediately on switching to it, without waiting on a backend refresh signal or a page reload.

    useChallengesSource gains a scoped refetch({ scope }), 'my-challenges' skips the catalog RPC, 'catalog' skips the GetChallengesInfo/history RPCs, 'all' (default, the initial load) fetches everything, and an in-flight guard so rapid tab toggling can't stack concurrent calls on the per-session backend. The refetch is silent: skeletons render only on an empty list, so refreshing over existing cards never flashes.

  • missions-hub: new goma:missions-count event reporting how many missions the widget holds

    A host that hides the widget's own tab strip (showTabs: false) draws its own chrome and had no way to badge it. The event carries { view, status, counts: { active, pending, finished, total, catalog } }, fires once the feed settles and again on every change (CTA mutation, refresh(), host-fed prop write), and never re-fires an identical payload.

    A feed a view-scoped rpc instance never fetches is reported as null rather than 0, so a host listening for both instances on one bubbling listener can tell "unknown" from "none" instead of clobbering a real count.

0.25.0
  • Missions widgets: BC design-parity pass over the hub and detail surfaces. The appearance object is now STRUCTURE-only (radii, border widths, pill geometry) while every colour moved to config.theme as a new set of mission --goma-* tokens (tag, unlock/value copy, level badge, level-card borders, and the Finished-tab history won accent --goma-missionHistoryWonColor). Adds a min-deposit to start field on cards (missions_min_deposit i18n key). The Finished-tab history cards start collapsed by default and their "Finished" pill renders green; a not-joined / not-started challenge now locks its whole level ladder instead of showing Level 1 active. Defaults reproduce the current look: a light-theme host reaches full parity via theme + appearance.
  • Missions widgets: new appearance styling object (el.appearance = { … } / appearance attribute) that maps curated keys to --goma-* CSS variables for pill/card/button geometry and pill/button/reward-icon colours. Defaults reproduce the current look; a light-theme host can now reach full design parity.
0.17.0
  • Challenges CTA write-path support: goma:mission-cta now carries challengeId + internalUserId (alongside missionId) and goma:mission-optout carries challengeId, so a host can drive the EM Challenges write endpoints (ChangeProgressStatus needs the progress id + internal user id; ChallengeForfeit needs the challenge id). A new public refresh() element method (backed by a reactive nonce in core) re-runs the widget's rpc data source on demand (<goma-missions-hub> re-fetches the enrolled + catalog feeds; <goma-mission-details> re-resolves the challenge by id) so a successful host-side mutation reflects in the UI without a reload. No-op in prop mode.

  • Mission details: missionId (the challenge's ChallengeId) resolves a single challenge in rpc mode via a server-side ChallengeId filter on the EM list feeds (GetChallengesInfo → DisplayableWidgetChallenges), so one request returns the full challenge shell. Enabling a refresh-safe standalone details route. The catalog (not-joined) resolution now also carries levels + rules so those details aren't empty. The Games tab paginates behind a "Load more" button (gamesPageSize, default 10) since GetChallengeGames returns up to 100 games in one call. <goma-missions-hub>'s goma:mission-select now also carries challengeId, the stable id a deep-link route resolves by.

  • Challenges (rpc mode): GetChallengesInfo is now the sole feed. DisplayableWidgetChallenges is no longer used. Each item carries two status fields: the player's progress Status and the challenge-lifecycle ChallengeStatus. The feed is split client-side: live not-started offers (progress Status 0 + ChallengeStatus Enabled) fill the Challenges catalog, every enrolled challenge (Status ≠ 0) fills My Challenges, and the by-id details route resolves a challenge (catalog or enrolled) from the same feed.

    <goma-mission-details>: the detail tabs now vary by challenge type. Casino shows Levels/Games/Rules, sports shows Levels/Events/Rules, both shows all four. The footer CTA is aligned to the hub card buttons (4px rounded, primary-accent outline / green solid). Quitting now opens an "Are you sure?" confirmation modal (native top-layer dialog with a backdrop fade + panel scale animation) before emitting goma:mission-optout; the Quit affordance itself is a red link.

    <goma-missions-hub>: a new stickyTop config (px) pins the Challenges | My Challenges tab strip below the host's fixed chrome as the list scrolls.

  • Missions hub: add the two top-level tabs, Challenges (a joinable catalog fed by DisplayableWidgetChallenges) and My Challenges, selectable via a new view prop, emitting goma:tab-change. Two-tab layout is opt-in via showTabs.

  • Missions cards get their production visual design. The hub's challenger card now renders faithful New, Deposit (not-joined), Suspended, Unavailable and Pending states: gold-bordered platform pill, status/countdown badges, an on-card level progress bar, and solid/outline CTAs. The Finished-tab history card renders Finished, Forfeit and Expired states with a Total-Won summary and a per-level timeline (completed / forfeited markers). <goma-mission-details> gets its redesigned header (banner countdown, level progress bar, end date). The EveryMatrix Challenges mappers derive the corresponding mission fields (status, level completion, totals) so the same designs render from live data.

  • Missions widgets gain an opt-in dataSource: 'rpc' mode that fetches live EveryMatrix Challenges data (read path). <goma-missions-hub> renders Active/Pending from DisplayableWidgetChallenges and Finished from PlayerHistory; <goma-mission-details> lazy-loads its Games tab. The default 'prop' (host-fed) path is unchanged.

  • Challenges hub cards: align the challenger-card visuals to the design. Status tags now read Suspended → blue, Ends-in → black; the on-card progress bar uses a solid green fill with a legible white centre label and an orange level node with a blue ring; and the CTA buttons are a 4px rounded rectangle (not a pill), with Resume/Pause rendered as an outline in the primary accent while Claim/Deposit keep the green solid fill. The tab pills and per-tab empty states are restyled to match, and empty states keep optional host illustrations while defaulting to a text-only layout.

    Enrolled-challenge mapping now matches the mobile clients: an active or suspended challenge exposes its current level + progress bar even at 0%, the level requirement uses the player's display-currency turnover target, and the active-tab CTA resolves to Resume/Pause from the challenge's paused state rather than always Start.

  • Missions Finished-tab history card fixes: the forfeited outcome now uses the shared alertError token consistently. The Forfeit pill, the per-level ✕ circle, and the "Forfeited" status text all paint --goma-alertError (replacing a maroon myTicketsLost pill and a hardcoded #be170f red). The token default is corrected to its design-system value #f9554d. The "X/Y levels" count now counts only levels the player actually completed, so a fully-forfeited challenge reads 0/4 instead of 4/4.

  • Challenges Finished tab: PlayerHistory authenticates with PlayerSession + UserId alone. No secure token (verified live). useChallengesSource now fetches the Finished list whenever userId is configured, dropping the previous secureToken requirement that left the tab empty. The getPlayerHistory API keeps the token as an optional header for any host that has one, but it is no longer needed.

  • Missions start/pause/resume CTA now follows the real ChangeProgressStatus state machine. The mapper exposes the raw per-player progressStatus, and both the hub card CTA (deriveCtaAction) and the detail footer drive off it: a Started(0) progress shows Start (it must be activated before it can be paused, deactivating it is rejected server-side as "wrong status"), an InProgress(1) one shows Pause, and a Paused(7) one shows Resume. Previously every enrolled active-tab challenge offered Pause, which failed for freshly-enrolled Started(0) challenges.

0.15.2
  • GomaPulse UX-review round 2: visual/token fixes:

    • core: official flat sports-icon set (46 mainstream/virtual/eSports-category glyphs across dark + light modes, keyed by sport id). Inherited app-wide via SbIcon; new casino_games_play i18n key (en/fr/pt).
    • casino-games: official flame glyph, hover "Play" overlay on tiles, title→tiles / tiles→dots spacing.
    • multi-banners: header now self-resolves the sport glyph from the event's sport id, renders the country flag via the shared CountryFlag component (new flagBaseUrl option, defaults to the OM flag CDN), and gains a 1px card border.
    • sports-navigation-horizontal: the rounded container now clips its content so the 8px corners render.
    • suggested-bet: leg sport icons use the flat bundled icon set.
    • game-details / missions-hub / sports-domain (My Bets): tabPill hover + pressed states (violet ring); My Bets ticket icon sizing (12px sport + flag) and centered totals summary.
0.10.0
  • Add the Missions widgets (Part 1, UI only, host-fed mock data):

    • <goma-missions-hub>: tabbed Active / Pending / Finished mission cards with per-tab empty states whose illustrations are host-injectable. Emits goma:mission-select, goma:mission-cta, goma:missions-offers.
    • <goma-mission-details>: banner + progress header, Levels / Games / Rules / Events tabs, and join-state variants (in-progress, not-joined, left-challenge). Follows goma:mission-select from a sibling hub; emits goma:mission-cta, goma:mission-optout, goma:mission-game-select, goma:mission-back.
    • sports-domain gains the shared Missions presentational components, useMissions helpers, and a useCountdown composable.
    • core locales gain the missions_* i18n keys (en / fr / pt).