<goma-mission-details>
The full detail view for one mission: a banner + progress header, type-specific tabs (a casino mission shows Levels / Games / Rules; a sports mission shows Levels / Events / Rules; both shows all four), and the join-state variants (in-progress, not-joined, left-challenge). The mission can be host-fed directly, resolved by following a sibling <goma-missions-hub>'s selection, or (in rpc mode) fetched by id from the EveryMatrix Challenges backend, which also unlocks a lazy-loaded Games tab.
Quitting is confirmation-gated: the Quit this mission link (Rules tab) only emits goma:mission-optout. It does not forfeit anything itself. The host owns the "Are you sure?" confirmation and the actual forfeit call, then tells the widget the forfeit succeeded via el.showForfeited(), which snapshots the mission and switches the view to the "You've left this mission" state. The Quit link only renders once the player has joined (an active/running mission); on a not-joined or pending mission the Rules tab shows no Quit link. Quit is also hidden while the progress is Started (Status 0, claimed but not opted in yet), like the EverMatrix widget.
Element tag
html
<goma-mission-details></goma-mission-details>Installation
bash
pnpm add @gomagaming/mission-details vue pinia vue-router vue-i18n @vueuse/core autobahn-browserSee Installation for the one-time .npmrc token setup. vue, pinia, vue-router, vue-i18n, @vueuse/core, and autobahn-browser are peerDependencies: installed once at the app level and deduped across every @gomagaming/* widget.
Configuration
js
const el = document.getElementById('detail')
el.config = {
locale: 'en',
// Only needed when dataSource is 'rpc'. See Data sources below.
apiBaseUrl: 'https://sports-api.example.com',
domainId: 123,
playerSession: 'abc123',
}| Key | Type | Required | Description |
|---|---|---|---|
locale | string | No | i18n locale (en / fr / pt). Defaults to 'en'. |
apiBaseUrl | string | For dataSource: 'rpc' | REST base URL for the EM Challenges API. Needed for both the by-id resolve (missionId set) and the Games-tab lazy fetch. |
domainId | string | number | For dataSource: 'rpc' by-id resolve | Sent as an integer DomainId on the challenge REST calls. Not needed for the Games-tab fetch, which only sends ChallengeId. |
playerSession | string | For dataSource: 'rpc' by-id resolve | Player session token forwarded as PlayerSession. Not needed for the Games-tab fetch. |
theme | object | No | --goma-* token overrides: see Theming. |
messages | object | No | Per-locale i18n overrides: see i18n / message overrides. |
debug | boolean | No | Verbose console logging. Defaults to false. |
Without apiBaseUrl (or with dataSource left at 'prop'), the by-id resolve and the Games-tab fetch both silently no-op: the widget stays on whatever mission the mission prop or the follow-select watcher supplies.
Data sources
Three independent ways the active mission gets into the widget, in the order the render logic actually resolves them (follow-select > by-id resolve > explicit prop):
- Direct prop: set
missionto aMissionobject (see the shape in the hub docs). - Follow a hub (default): leave
followMissionSelecton and supply amissionspool; when a sibling<goma-missions-hub>firesgoma:mission-select, this widget looks updetail.missionIdagainst the pool (falling back todetail.missionif the id isn't in the pool) and renders it. See Integrates with. rpcby id: setdataSource: 'rpc'and amissionId(the mission's ChallengeId), withconfig.apiBaseUrl/domainId/playerSessionset and nomissionprop or hub in sight. The widget resolves that one mission by passing the id as a server-sideChallengeIdfilter to the feed chosen bymissionSource:'my-challenges'(default) →GetChallengesInfo(noIsFutureAvailableflag, so the server returns the challenge whichever Active/Pending bucket it's in; if that flagless call comes back empty it retries once withIsFutureAvailable: truefor the Pending bucket before concluding not-available);'catalog'→DisplayableWidgetChallenges(the joinable offers). There is no cross-feed fallback: a catalog offer lives only inDisplayableWidgetChallenges, and a challenge the player forfeited (quit) is gone fromGetChallengesInfo, so a'my-challenges'miss shows the "no longer available" state instead of being resurrected as a claimable catalog offer. SetmissionSourcefrom the tab the details were opened from, and keep it in the URL (?source=catalog) so it survives a refresh/deep-link. This enables a refresh-safe standalone/…/:iddetails route.missionIdis empty ('') by default, which falls back to the follow-select / explicit-prop behaviour above.
Independently of how the mission arrives, in dataSource: 'rpc' mode opening the Games tab lazy-loads that mission's game tiles from GetChallengeGames (once per challengeId/id, cached until it changes): this only needs config.apiBaseUrl. Games render as a 3-column grid of square, artwork-only thumbnails (the game name rides on each tile's aria-label / image alt); a Load more button reveals additional pages behind gamesPageSize.
Props / attributes
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
mission | mission | Mission | null | null | The mission to render (prop mode). |
missions | missions | Mission[] | [] | Pool the follow-select watcher resolves ids against. |
followMissionSelect | follow-mission-select | boolean | true | Follow a sibling hub's goma:mission-select. |
missionSelectSelector | mission-select-selector | string | '' | Optional CSS selector to only follow a specific hub instance. |
activeDetailTab | active-detail-tab | 'levels'|'games'|'rules'|'events' | 'levels' | Initial tab. Falls back to 'levels' if the value isn't offered for the mission's platform. |
leftStateImage | left-state-image | string | '' | Host-injectable illustration for the "left this mission" card. |
dataSource | data-source | 'prop'|'rpc' | 'prop' | 'rpc' enables the by-id resolve (with missionId) and the Games-tab lazy fetch. |
missionId | mission-id | string | '' | The mission's ChallengeId. With dataSource: 'rpc', resolves that single mission: see Data sources. |
missionSource | mission-source | 'my-challenges'|'catalog' | 'my-challenges' | Which feed the by-id resolve queries (rpc mode). 'my-challenges' → GetChallengesInfo (the player's enrolled/quit challenges); 'catalog' → DisplayableWidgetChallenges (the joinable offers). Set this from the tab the details were opened from: a catalog offer lives only in DisplayableWidgetChallenges, while a forfeited challenge is gone from GetChallengesInfo and must show the not-available state rather than being resurrected as claimable. Each source queries exactly one feed (no cross-fallback). Persist it in the URL (e.g. ?source=catalog) so it survives a refresh/deep-link. |
gamesPageSize | games-page-size | number | 9 | Games-tab page size (rpc mode). 0 shows all fetched games at once. |
colorMode | color-mode | 'dark'|'light' | 'dark' | Toggles the root dark class so the widget can follow the host app's light/dark theme. |
Appearance (styling object)
appearance carries structure only: radii and other mode-agnostic single values that map to one --goma-* CSS custom property each, 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 (footer CTA button colours, the reward/requirement glyphs, and the Levels-tab level 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-mission-details> shares the same structure map as <goma-missions-hub>, so you can pass a host's single appearance object to both, but each widget only applies the keys that map onto surfaces it renders. The structure keys that take effect on <goma-mission-details> are:
| Key | CSS variable | Affects |
|---|---|---|
buttonRadius | --goma-radiusButton | Footer CTA button corner radius. |
levelCardRadius | --goma-missionLevelCardRadius | Levels-tab level-card corner radius. |
gamesColumns | --goma-gamesColumns | Columns of the Games tab grid on <goma-mission-details> (tiles and skeletons). Default 3. |
gameTileRatio | --goma-gameTileRatio | Aspect ratio of a game tile, for example 168 / 146. Default 1 / 1. |
detailTabsFullWidth | --goma-detailTabsFullWidth | true removes the 16px side padding of the detail tab strip. Default false. |
detailTabIndicatorHeight | --goma-detailTabIndicatorHeight | Height of the ACTIVE detail tab underline only. Inactive tabs draw the 1px missionDetailTabTrack line. Default 2px. |
fontFamily | --goma-fontFamily | Font of the widget text. Default the widget font. inherit takes the host font. |
No-op structure keys on this widget
The shared map also carries cardRadius and the pill keys (pillRadius, pillActiveBorderWidth, pillFontWeight, pillActiveFontWeight, pillHeight, pillPaddingX). These have no effect on <goma-mission-details>. Its type tabs (Levels / Games / Rules / Events) are an underline strip, not pills, and its non-level cards use a fixed corner radius. Those keys only style <goma-missions-hub> (pill tab strip, cardRadius mission cards). Passing them here is harmless: they're simply ignored. buttonRadius and levelCardRadius are the geometry knobs that apply to this widget.
Level colours still apply here
Even though the pill/cardRadius structure keys are no-ops, this widget's Levels tab does read the mission colour tokens: missionRewardIcon, missionRequirementIcon, missionLevelBadgeBg / …BorderColor / …Text, missionLevelActiveBorderStart / …End, and missionLevelCompletedBorderColor. Those are set through config.theme, not appearance. See the Theming guide.
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 (Rules tab → "Quit this challenge") is rendered by the host, not the widget. The widget 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. This is the same pair a host would hand <goma-missions-hub>; on <goma-mission-details> only the button + level keys take effect (the pill / cardRadius structure keys are ignored here, see the note above):
js
// structure (mode-agnostic single values)
el.appearance = {
buttonRadius: '9999px',
levelCardRadius: '20px',
// ignored here, but style <goma-missions-hub> when the object is shared:
cardRadius: '0.75rem',
pillRadius: '9999px',
}
// colours (need light/dark parity → config.theme)
el.theme = {
missionButtonBg: '#40b840',
missionButtonText: '#ffffff',
missionButtonBorderColor: '#40b840',
missionRewardIcon: '#40b840',
missionRequirementIcon: '#4a90e2',
}Events
All events are composed + bubbling.
| Event | detail | When |
|---|---|---|
goma:mission-cta | { missionId, challengeId, internalUserId, action } | Footer CTA pressed, or the "left this mission" card's own action link tapped. action is derived from join state ('start' / 'pause' / 'resume' for an enrolled mission, 'discover' once forfeited) or, for a not-yet-joined mission, an explicit mission.cta override (e.g. 'deposit' / 'launch' / 'claim'). missionId is the per-enrollment progress id; challengeId + internalUserId support the write endpoints. |
goma:mission-optout | { missionId, challengeId } | Quit confirmed by the host: actually fired the moment the Rules tab's "Quit this mission" link is tapped (the widget has no confirmation modal of its own; see the note above). challengeId is what a forfeit call needs. |
goma:mission-game-select | { missionId, game } | A game tile (Games tab) tapped. In rpc mode game is the full raw casino game object (GameLaunchUrl, GameSlug, GameName, GameId, CasinoGameId, Vendor, …) so the host can launch it directly; in prop mode it's whatever object the host put in mission.games. |
goma:mission-back | { missionId } | Header back chevron pressed. |
goma:retry | {} | User taps the retry button on the render-error banner. |
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. |
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 open challenge 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.
| Time | What happens |
|---|---|
| 0 s | The player taps Pause. The host sends the write and calls setProgressStatus(id, 'paused'). The card shows Suspended at once. |
| 1.5 s | The host calls refresh(). |
| 1.5 s, server agrees | The fetched item has the Paused status. The instant change ends. Done. |
| 1.5 s, server does not agree yet | The card keeps Suspended. |
| 10 s | The 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. |
Methods
| Method | Description |
|---|---|
refresh() | Re-resolves the mission by id in dataSource: 'rpc' mode (with missionId set). Call it after a successful host-side CTA / quit mutation so the header, progress and join-state reflect the new server state without a reload. No-op in 'prop' / follow-select mode. |
showForfeited() | One-shot signal telling the widget a host-driven forfeit succeeded. Snapshots the current mission with joinState: 'forfeited' so the "You've left this mission" state renders even after the next reconcile refetch drops the mission from the feed. |
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 <goma-mission-details>, 'forfeited' on the shown mission shows the same "You've left this Challenge" state as showForfeited(). 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
Listens for goma:mission-select (gated by followMissionSelect, default true, optionally scoped to one hub via missionSelectSelector) and shows the selected mission. This is the pairing the widget is built around: <goma-missions-hub> upstream fires goma:mission-select when a mission card is tapped, and this widget resolves the id against its missions pool (or falls back to the event's inline mission payload) with no extra glue code. See Cross-widget wiring for the full payload contract and the emitter/listener matrix.
goma:mission-cta, goma:mission-optout, goma:mission-game-select, and goma:mission-back are host-only today: no other shipped widget listens for them. The widget reads and writes no shared-storage envelope (no betslip, favourites, or session state).
Empty / loading / error states
- No mission resolved: before any of the three data paths produce a mission (and once resolution has settled with nothing found), the widget shows a centred
missions_no_levelsmessage. - Resolving by id (rpc), while
dataSource: 'rpc'andmissionIdis set but no mission has arrived yet (and no by-id error and no forfeited snapshot), aMissionDetailsSkeletonrenders in place of the header, tabs and levels list. - Games tab loading (rpc): until
GetChallengeGameshas returned for THIS mission, the Games tab shows only the skeleton grid, even when the mission object carries games. After the response it shows exactly the fetched games, or the empty state. Switching to another mission clears the old games at once, so they never show under the new mission. In'prop'mode the games the host passes show directly. - Per-tab empty states: a mission with no
levels/games/eventsshows that tab's own "nothing to show" message (missions_no_levels/missions_no_games/missions_no_events). - Status badge: a Suspended (blue) or Unavailable (grey) mission shows that pill top-left on the header banner, the same
ColorTagand text as the list card. It replaces the "Ends in" countdown. Forfeited still wins. The footer CTA rules do not change. - Not-joined / not-started mission, until the player joins (the mission is enrolled and running), the Levels tab renders the whole ladder locked, there is no active Level 1; every level shows its locked state. Joining activates the current level.
- Left this mission: after
el.showForfeited()(or a resolved mission whosejoinStateis already'forfeited'), the Levels tab renders the "You've left this mission" card instead of the levels list, with its own CTA that also emitsgoma:mission-cta(action: 'discover'). - Render error: an
onErrorCapturedboundary catches a Vue render error, shows a red inline banner (ERROR: <message>) with a Retry button (emitsgoma:retry) and a dismiss×, and auto-clears after 8000 ms even without a retry tap.
Theming
The core --goma-* tokens this widget's own components read (the curated playground picker set lives in packages/mission-details/src/themeTokens.js; the Levels-tab colour tokens below are additional): see Theming for the complete reference. Every one is a colour set via config.theme:
- Backgrounds:
--goma-backgroundPrimary(page),--goma-backgroundSecondary(header/back-bar),--goma-backgroundCards(level cards),--goma-backgroundBorder(empty-state border, "no levels/games/events" and "left this mission"),--goma-separatorLine(tab underline, dividers, header progress-bar track). - Text:
--goma-textHeadlinePrimary(headings),--goma-textPrimary(primary text / active tab),--goma-textSecondary(secondary text / inactive tab). - Icons:
--goma-iconPrimary(back chevron / carets). - Highlights:
--goma-highlightPrimary(not-joined unlock prompt),--goma-highlightPrimaryContrast(focus ring),--goma-highlightSecondary(Terms & Conditions link; completed-level label and border; header progress-bar fill end),--goma-highlightTertiary(Read-full-terms link; header progress-bar fill start; active per-level mini-bar gradient start). - Buttons,
--goma-allWhite(the header's "ends-in" countdown status pill, background + border, via the sharedColorTagcomponent; its label text is--goma-allDark, not this token. Once the mission is forfeited the banner shows a "Forfeited" pill in--goma-alertErrorinstead, and the countdown is not rendered at all),--goma-allDark(header progress-bar center label; also the "ends-in" pill's text, above),--goma-missionButtonBg/--goma-missionButtonText(solid footer CTA fill/text for join actions),--goma-missionButtonBorderColor(outline footer CTA border/text for resume/view/pause actions, and the Games-tab "Load more" button). - Alerts:
--goma-alertError("Quit this mission" link; render-error banner). - Levels-tab level colours: dedicated tokens the level cards read, each set via
config.theme:--goma-missionLevelBadgeBg/--goma-missionLevelBadgeBorderColor/--goma-missionLevelBadgeText(level-number progress badge, also the header progress-bar's current-level node, since the header reuses the same progress component as<goma-missions-hub>'s cards, not a dedicated bar),--goma-missionLevelActiveBorderStart/--goma-missionLevelActiveBorderEnd(active card gradient border),--goma-missionLevelCompletedBorderColor(completed card border), and--goma-missionRewardIcon/--goma-missionRequirementIcon(reward/ requirement glyphs). Their defaults resolve to the highlight greens above. The card corner radius is the one structure knob here:--goma-missionLevelCardRadius(default20px), set viaappearance.levelCardRadius. - Sticky footer placement:
--goma-stickyFooterOffsetand--goma-stickyFooterZIndex. Neither has a default, so the footer CTA sits flush to the bottom of the viewport with automatic stacking unless you say otherwise. See Keeping the CTA clear of your own bottom chrome below.
Keeping the CTA clear of your own bottom chrome
The footer CTA is position: sticky; bottom: 0, and that 0 resolves against the viewport, not against your layout. If your page carries its own fixed bottom chrome (a nav bar, a tab strip, a cookie banner) the CTA scrolls underneath it, because the widget renders in a shadow root and has no way to measure something outside itself.
Tell it how much room to leave:
css
goma-mission-details {
--goma-stickyFooterOffset: 56px; /* your nav bar's height */
}If your chrome also sets a z-index, lift the footer above it:
css
goma-mission-details {
--goma-stickyFooterOffset: 56px;
--goma-stickyFooterZIndex: 20; /* above your nav bar's z-index */
}Both also work through config.theme (stickyFooterOffset / stickyFooterZIndex) if you drive theming from JavaScript.
js
el.theme = {
backgroundPrimary: '#03061b',
backgroundCards: '#1f2147',
highlightPrimary: '#ff6600',
}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 token | Elements | Default | When to set it |
|---|---|---|---|
pillActiveText | Selected sub-tab pill label | #ffffff | Send it when the design draws this element with a variable other than the default. |
pillActiveBg | Selected sub-tab pill fill | #2a2d60 | Send it when the design draws this element with a variable other than the default. |
missionButtonBg | Solid CTA fill (hub card, details footer) | var(--goma-allWhite) | Send it when the design draws this element with a variable other than the default. |
missionButtonText | Solid CTA label | var(--goma-allDark) | Send it when the design draws this element with a variable other than the default. |
missionButtonBorderColor | Outline CTA border, and the Games "Load more" border and label | var(--goma-allWhite) | Send it when the design draws this element with a variable other than the default. |
missionCtaOutlineText (new) | Outline CTA label | var(--goma-missionButtonBorderColor) | Send it when the design draws this element with a variable other than the default. |
missionDetailTabIndicator (new) | Active underline of the detail tabs | var(--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 own | var(--goma-separatorLine) | Send transparent when the design draws no line for inactive tabs. |
missionUnlockText | Unlock or description line on a not-joined card | var(--goma-highlightTertiary) | Send it when the design draws this element with a variable other than the default. |
missionValueText | Value 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. |
missionLevelBadgeText | Current level number on the card progress bar | var(--goma-textPrimary) | Send it when the design draws this element with a variable other than the default. |
missionTagBorderColor | Border of the Casino or Sports pill | var(--goma-highlightTertiary) | Send it when the design draws this element with a variable other than the default. |
missionHistoryWonColor | Finished tab: TOTAL WON border and value, completed level circle, reward and status | var(--goma-highlightTertiary) | Send it when the design draws this element with a variable other than the default. |
missionLevelActiveBorderStart / missionLevelActiveBorderEnd | Two stops of the active level card gradient border | highlightTertiary / highlightSecondary | Send two variables for a two-color border, or the same variable twice for one color. |
missionRulesTermsLink (new) | "Read full terms" link on the Rules tab | var(--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 highlightSecondary | var(--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 card | var(--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 header | var(--goma-highlightPrimary) | Send it when the design draws this element with a variable other than the default. |
missionDetailTabActiveText (new) | Label of the active detail tab | var(--goma-textPrimary) | Send it when the design draws this element with a variable other than the default. |
missionDetailsBg (new) | Page surface behind the details page (the sticky footer bar keeps backgroundPrimary) | var(--goma-backgroundPrimary) | Send it when the design draws this element with a variable other than the default. |
missionEventsPercent (new) | Contribution percent on the Events tab below 100% | var(--goma-highlightPrimary) | Send it when the design draws this element with a variable other than the default. |
missionEventsPercentComplete (new) | Contribution percent on the Events tab at 100% or more | var(--goma-highlightTertiary) | Send it when the design draws this element with a variable other than the default. |
missionLevelCompletedText (new) | "Completed" label on a completed level card | var(--goma-highlightSecondary) | Send the variable the design uses for this label. |
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 token | Element | Default (light) | Was |
|---|---|---|---|
missionTagBgStart / missionTagBgEnd (new) | Casino or Sports pill gradient, left to right | var(--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. Five 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.
| Key | CSS variable | Default | When to set it |
|---|---|---|---|
gamesColumns | --goma-gamesColumns | 3 | Send 2 for a two-column tile grid. |
gameTileRatio | --goma-gameTileRatio | 1 / 1 | Send the tile aspect ratio the design draws, for example 168 / 146. |
detailTabsFullWidth | --goma-detailTabsFullWidth | false | Send true when the design runs the tab strip edge to edge. |
detailTabIndicatorHeight | --goma-detailTabIndicatorHeight | 2px | Send the underline height the design draws, for example 6px. |
fontFamily | --goma-fontFamily | the widget font | Send 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. The tab label does not move when the 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.
| Key | Accepted value |
|---|---|
gamesColumns | A positive integer, for example 2. |
gameTileRatio | A CSS aspect-ratio, for example 1 / 1 or 168 / 146. Not 16:9. |
detailTabsFullWidth | A boolean, true or false. Not the string 'false'. |
detailTabIndicatorHeight | A CSS length, for example 2px. |
fontFamily | A 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.
| Component | Element | Old | New |
|---|---|---|---|
| Hub page | Sticky top tab band fill | backgroundCards | backgroundTertiary |
| Hub page | Challenges and My Challenges headings | textHeadlinePrimary | textPrimary |
| Top tabs | Strip underline, inactive underline | separatorLine, transparent | separatorLineSecondary |
| Top tabs | Inactive label | textSecondary | textPrimary |
| Sub-tab pills | Resting pill fill | pills | pillBg (default pills) |
| Details page | Back label and chevron | textHeadlinePrimary | textPrimary |
| Challenger card, header, finished card | Terms & Conditions link | highlightSecondary | missionTermsLink (default highlightSecondary) |
| Challenger header | Not-joined unlock prompt | highlightPrimary | missionHeaderUnlockText (default highlightPrimary) |
| Details page | Page surface | backgroundPrimary | missionDetailsBg (default backgroundPrimary) |
| Detail tabs | Active label | textPrimary | missionDetailTabActiveText (default textPrimary) |
| Detail tabs | Inactive underline, inactive label | transparent, textSecondary | separatorLine, textPrimary |
| Challenger card | Title | textHeadlinePrimary | textPrimary |
| Card progress bar | Track | separatorLine | scroll |
| Card progress bar | Centre label | allDark | highlightSecondaryContrast |
| Card progress bar | Next level node fill | separatorLine | backgroundBorder |
| Platform pill | Label | allWhite | textPrimary |
| Status tag, ends-in | Fill, label, border | allWhite, allDark, allWhite | highlightPrimaryContrast, highlightSecondaryContrast, shadow |
| Status tag, suspended | Label | allDark | highlightSecondaryContrast |
| Status tag, finished | Fill, label | highlightTertiary, allWhite | highlightSecondary, highlightSecondaryContrast |
| Status tag, forfeit | Fill, label | alertError, allWhite | myTicketsLost, highlightPrimaryContrast |
| Status tag, expired | Label | allWhite | highlightPrimaryContrast |
| Status tag, new | Label, border | allWhite, allWhite | highlightPrimaryContrast, shadow |
| Challenge header | Title | textHeadlinePrimary | textPrimary |
| Empty state | Title; card variant border; text-only fill | textHeadlinePrimary; separatorLine; backgroundCards | textPrimary; backgroundBorder; backgroundTertiary |
| Empty state | Text-only CTA link | highlightSecondary | highlightTertiary |
| Rules panel | Contribution type and rule values | textSecondary | textPrimary |
| Events panel | Contribution percent below 100% / at 100% or more | highlightPrimary / highlightTertiary | missionEventsPercent (default highlightPrimary) / missionEventsPercentComplete (default highlightTertiary) |
| History card | Outer border; title | separatorLine; textHeadlinePrimary | backgroundBorder; textPrimary |
| Level card | Active card fill; "Completed" label; progress track | backgroundCards; highlightSecondary; separatorLine | backgroundSecondary; missionLevelCompletedText (default highlightSecondary); scroll |
i18n / message overrides
Keys this widget's own components reference (bundled en defaults shown):
back("Back"),retry("Retry"),terms_conditions("Terms & Conditions")missions_levels_tab/missions_games_tab/missions_rules_tab/missions_events_tab: tab labelsmissions_level("Level"),missions_completed("Completed")missions_end_date("End date"),missions_ends_in("Ends in")missions_forfeit_header: shown in place of the progress bar once forfeitedmissions_contribution/missions_contribution_type/missions_contribution_rule: Rules and Events tab copymissions_odds_rules("Odds:"): Rules-tab heading for a sports challenge's odds limits. One line per limited bet type:missions_odds_single,missions_odds_multiple,missions_odds_multiple_selection,missions_odds_system,missions_odds_system_selection(each takes a{range}). The range itself ismissions_odds_between("min. {min} · max. {max}"),missions_odds_minormissions_odds_maxwhen only one bound is setmissions_read_full_terms,missions_quit("Quit this mission")missions_no_levels/missions_no_games/missions_no_events: per-tab and top-level empty statesmissions_load_more: Games-tab pagination buttonmissions_left_title/missions_left_subtitle: the "left this mission" cardmissions_cta_start/_pause/_resume/_deposit/_discover/_launch/_claim/_view: footer CTA labels, keyed off the derivedaction
Pass config.messages to override:
js
el.config = {
locale: 'en',
messages: {
en: { missions_quit: 'Leave this mission' },
},
}Embedding examples
Vanilla: paired with the hub
html
<goma-missions-hub id="hub"></goma-missions-hub>
<goma-mission-details id="detail"></goma-mission-details>
<script type="module">
import '@gomagaming/missions-hub'
import '@gomagaming/mission-details'
const missions = [/* Mission[] */]
hub.missions = missions
detail.missions = missions // lets the follow-select watcher resolve ids
</script>React
jsx
import '@gomagaming/mission-details'
<goma-mission-details
ref={(el) => { if (el) el.mission = activeMission }}
onGoma:mission-cta={(e) => handleCta(e.detail)}
/>Vue
vue
<script setup>
import '@gomagaming/mission-details'
import { ref } from 'vue'
const mission = ref(/* Mission */)
</script>
<template>
<goma-mission-details :mission.prop="mission" @goma:mission-back="goBack" />
</template>Accessibility
- The detail tab strip is an ARIA
tablist; each tab is arole="tab"witharia-selected. - The header's in-progress bar is a
role="progressbar"witharia-valuenow/aria-valuemin/aria-valuemaxand anaria-labelcarrying the centre status text. - The back button carries an
aria-label; the render-error banner isrole="alert". - Each Games-tab tile is a
<button>witharia-labelandtitleset to the game name, and analt-tagged, lazy-loaded image. - Interactive elements (back button, tabs, footer CTA, quit link, retry button, game tiles, load-more button) all carry a visible
focus-visiblering keyed to--goma-highlightPrimaryContrast. - Decorative glyphs (the lock icon on a locked level, the requirement sword icon) are
aria-hidden="true".
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: corrected the same stale
--goma-buttonActiveHoverTertiarydoc claim as missions-hub (shared header component), plus a second stale--goma-allWhiteclaim found while fixing it.Initialization:
autobahn-browseris a required peer dependency even though this widget'srpcmode is plain REST; documented why, matching highlight-card/pros-choice's phrasing.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.4
<goma-mission-details>: the Rules tab of a sports challenge now lists its odds limits under an "Odds:" heading, one line per bet type (for example "Single bets: min. 2 · max. 3"). The lines come from the challenge'sSportSettingsodds ranges (total odds for single, multiple and system bets, and odds per selection for multiple and system bets); a range with no bound set is not shown.@gomagaming/sports-domain: challenge missions now carryrules.odds, an array of localized lines (empty when the challenge sets no odds limits). Odds values are formatted for the widget's locale, sofrandptread1,5.@gomagaming/core: newmissions_odds_*i18n keys inen,frandpt.
0.37.2
The Challenges widgets (
<goma-missions-hub>and<goma-mission-details>) now read Figma color tokens, as a light class and adark:class. New optional role tokens, each with a-darktwin:pillBg,missionCtaOutlineText,missionDetailTabIndicator,missionDetailTabTrack,missionTopTabTrack(defaultseparatorLine),missionTopTabInactiveLine(default transparent),missionRulesTermsLink,missionProgressFillStart,missionEventsPercent,missionEventsPercentComplete,missionLevelCompletedText(defaulthighlightSecondary),missionTermsLink,missionHeaderUnlockText,missionDetailTabActiveText,missionHubBg,missionDetailsBg,missionPillsBarBg,missionTagBgStartandmissionTagBgEnd. New optionalappearancekeys:gamesColumns,gameTileRatio,detailTabsFullWidth,detailTabIndicatorHeight,topTabIndicatorHeight,pillsBarPaddingBottomandfontFamily. AfontFamilythat the host sets throughthemeis no longer removed whenappearancedoes not set it.New element methods on both elements, for an optimistic update right after a write:
markClaimed,setProgressStatusandsetChallengeAvailable. New@gomagaming/sports-domainexports:markClaimed,setProgressStatus,setChallengeAvailable,isClaimed,getStatusOverride,applyStatusToMission,applyAvailabilityToMission,isForfeitedOverride,filterClaimedandOVERRIDE_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
rpcmodegoma:missions-countnow sendstotal: null, andactive,pendingandfinishedstay 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
-darktwin that defaults to its light value, so a host that sets only the light value still paints both modes:pillText,pillActiveBorderColor,missionLevelBadgeBg,missionLevelBadgeBorderColorandmissionLevelCompletedBorderColor.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:
#939dffto white (textPrimary). - Card progress track and level progress track:
#2a2d60to#393d83(scroll). - Active level card fill:
#1f2147to#161732(backgroundSecondary). - ColorTag
ends-inandnewborder: white to#03061b(shadow). - ColorTag
finished: green fill with white text to#404cfffill (highlightSecondary) with#03061btext (highlightSecondaryContrast). - ColorTag
forfeitfill:#f9554dto#60111b(myTicketsLost). - Rules tab contribution text:
#939dffto white (textPrimary). - Inactive detail tab label:
#939dffto white (textPrimary). - Empty state text link:
#404cffto#01b72e(highlightTertiary).
- Inactive top tab label:
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
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.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
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
textSecondaryon 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 measuredrgb(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
currentColorexactly 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 istextPrimarywhite, 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-pillActiveRingsets 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-activerule and a--goma-tabPillActivetoken that nothing referenced. The rule carried a hardcoded gradient no widget ever applied, and its.darkvariant could not have matched under any circumstances, since dark mode swaps the--goma-*values rather than adding a.darkclass.The mission-detail footer CTA now renders its label in uppercase, matching the design.
The detail view's sticky footer CTA resolves
bottom: 0against 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-stickyFooterOffsetreserves the space and--goma-stickyFooterZIndexsettles 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
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.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.25.2
Mission details: specify which feed the by-id resolve queries (fixes catalog details).
<goma-mission-details>gains amissionSourceproperty ('my-challenges'default |'catalog') so the by-id resolve queries exactly the feed the details were opened from:'catalog'→DisplayableWidgetChallenges(a Défis offer lives only there),'my-challenges'→GetChallengesInfo. This fixes a catalog challenge being unopenable after the previous "GetChallengesInfo-only" change, without reintroducing a blind fallback: a quit challenge stays "no longer available" instead of resurfacing as claimable. Persist it in the URL (?source=catalog) so it survives a refresh/deep-link.Also hardens the by-id resolve:
- Pending safety net: the
my-challengeslookup does the flaglessGetChallengesInfoand, if empty, retries once withIsFutureAvailable: true(the Pending bucket) before concluding not-available. Never touches the catalog. - Config-not-ready timeout (15s): a
missionIdset with a config that never supplies a usable session no longer skeletons forever; it surfaces a retryable error. - Error state: a by-id fetch failure renders a retryable error (
missions_detail_error, en/fr/pt) instead of masquerading as the empty "no levels" state.
- Pending safety net: the
0.25.1
Mission details: resolve a challenge by id from GetChallengesInfo only.
<goma-mission-details>(rpc, by-id) now reads the challenge from a singleGetChallengesInfocall, server-filtered to the oneChallengeId, with noIsFutureAvailableflag so the server returns it whichever bucket (Active/Pending) it's in, and noDisplayableWidgetChallengesfallback. A forfeited (quit) challenge drops out ofGetChallengesInfo; the old catalog fallback resurrected it as a claimable offer, so quitting then re-opening the details showed a "claim" button. WhenGetChallengesInforeturns nothing the view now shows a "This challenge is no longer available." state (new i18n keymissions_challenge_unavailable, en/fr/pt) instead.Also fixes a latent infinite-skeleton: the by-id resolve now exposes
notFoundso the details stop the loading skeleton when the challenge genuinely isn't returned.createChallengeApi().getChallengesInfoomitsIsFutureAvailablefrom the request body when the caller doesn't pass it (the hub feeds still passfalse/truefor the Active/Pending split).
0.25.0
- Missions widgets: new
appearancestyling object (el.appearance = { … }/appearanceattribute) 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.18.0
<goma-mission-details>now emits the full casino game object when a Games-tab tile is tapped.goma:mission-game-selectcarries{ missionId, game }, wheregameis the whole casino game (GameLaunchUrl,GameSlug,GameName,GameId,CasinoGameId,Vendor, …): the challenge-games mapper preserves the raw object so the host can launch the game directly from the payload. The Games-tab "Load more" button adopts the mission secondary-button style (white outline pill) instead of the highlight-primary outline.
0.17.0
Challenges CTA write-path support:
goma:mission-ctanow carrieschallengeId+internalUserId(alongsidemissionId) andgoma:mission-optoutcarrieschallengeId, so a host can drive the EM Challenges write endpoints (ChangeProgressStatusneeds the progress id + internal user id;ChallengeForfeitneeds the challenge id). A new publicrefresh()element method (backed by a reactive nonce in core) re-runs the widget'srpcdata 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 inpropmode.Mission details:
missionId(the challenge's ChallengeId) resolves a single challenge in rpc mode via a server-sideChallengeIdfilter 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) sinceGetChallengeGamesreturns up to 100 games in one call.<goma-missions-hub>'sgoma:mission-selectnow also carrieschallengeId, 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
Statusand the challenge-lifecycleChallengeStatus. 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 emittinggoma:mission-optout; the Quit affordance itself is a red link.<goma-missions-hub>: a newstickyTopconfig (px) pins the Challenges | My Challenges tab strip below the host's fixed chrome as the list scrolls.Challenges data + details overhaul.
- Feeds split by source. The Challenges catalog now comes from
DisplayableWidgetChallenges(the not-yet-joined offers); My Challenges Active / Pending come fromGetChallengesInfo, called twice, theIsFutureAvailableflag decides the sub-tab (false→ Active,true→ Pending), and Finished fromPlayerHistory. The three feeds are independent, so a catalog or history failure leaves the rest intact. - CTA driven by the challenge's triggers. A catalog card's button and action derive from the challenge's enabled
Triggers(priority Deposit > Game Launch > Claim →deposit/launch/claim), with a newlaunchlabel. - Rules & Events. The Rules panel now renders the challenge description, the rules text before the T&C link, and the contribution rule; a sports challenge additionally renders an Events tab from the challenge's per-event contribution rules.
- Games tab is a 3-column grid of square, artwork-only thumbnails (the game name rides on each tile's
aria-label/ imagealt); page size is now 9 (three full rows) and the "Load more" button matches the app's secondary-button styling. - Left / forfeited state. Quitting a challenge shows an optimistic left-state card and the details header drops the end date while keeping the T&C link;
PlayerHistorylevel completion is read from each level'sIsCompletedso a forfeited challenge no longer reports as completed.
- Feeds split by source. The Challenges catalog now comes from
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.Missions Finished-tab history card fixes: the forfeited outcome now uses the shared
alertErrortoken consistently. The Forfeit pill, the per-level ✕ circle, and the "Forfeited" status text all paint--goma-alertError(replacing a maroonmyTicketsLostpill and a hardcoded#be170fred). 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 reads0/4instead of4/4.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: aStarted(0)progress shows Start (it must be activated before it can be paused, deactivating it is rejected server-side as "wrong status"), anInProgress(1)one shows Pause, and aPaused(7)one shows Resume. Previously every enrolled active-tab challenge offered Pause, which failed for freshly-enrolledStarted(0)challenges.
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. Emitsgoma: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). Followsgoma:mission-selectfrom a sibling hub; emitsgoma:mission-cta,goma:mission-optout,goma:mission-game-select,goma:mission-back.sports-domaingains the shared Missions presentational components,useMissionshelpers, and auseCountdowncomposable.corelocales gain themissions_*i18n keys (en / fr / pt).