Skip to main content

Message Catalog — Design Spec

Date: 2026-07-17 Branch: feat/message-refactor Scope: strykr-fe (frontend)

Problem

User-facing toast/notification copy is hardcoded inline across many files (useAuth.ts, InlineBetSlip.tsx, useWebSocket.ts, useOpenBets.ts, notificationStore.ts, orderStatus.ts, api.ts). The same concerns are scattered: what text to show, which type (icon + colour) to use, whether to show a toast at all, and how backend error codes map to friendly strings. This makes copy changes error-prone (the driving Excel has ~20 edits across stale line numbers) and makes it impossible to see all user-facing messages in one place.

Goals

  1. One catalog holding every user-facing message — both backend-mapped (error code → friendly string) and frontend-only (toasts, inline validation).
  2. Each entry carries not just text but the full presentation: type (colour), title, message, duration, optional icon override, and the ability to express "no toast".
  3. Support dynamic messages (e.g. Back/Lay "selection" @ "matched_odds", Min stake is <limit>, Bet Partially Matched (<pct>%)).
  4. Call sites reference the catalog instead of hardcoding strings.
  5. No new runtime dependency; generalize patterns already trusted in the repo.

Non-Goals

  • Multi-language / locale support (no i18n library — YAGNI for a single-locale app).
  • Changing when/whether events fire on the backend (that is content, tracked separately — see Dependencies).
  • Restyling toasts beyond the single icon-override change needed for Tick+Yellow.

Existing patterns this generalizes

  • ERROR_MESSAGES (src/lib/api.ts:242) — backend code → friendly string map, resolved by parseApiError (api.ts:256).
  • classifyOrderStatusToast (src/lib/orderStatus.ts:120) — pure, unit-tested function mapping a status to { kind, title, body } or null (no toast).
  • toast (src/store/notificationStore.ts) — success|error|warning|info convenience wrappers; rendered by src/components/ui/Toast.tsx where type drives BOTH icon and colour (they are coupled today).

Architecture

Module layout — src/lib/messages/

src/lib/messages/
├── types.ts ToastDescriptor, ToastTone, ToastIcon
├── auth.ts auth.* entries
├── placement.ts inline + slip placement entries (incl. stakeError.*)
├── lifecycle.ts matched / settled / re-settle / server-notification entries
├── cancel.ts open-bet cancel entries
├── errors.ts byCode map (absorbs ERROR_MESSAGES) + http fallbacks
└── index.ts export const messages = { auth, placement, lifecycle, cancel, errors }

Split by domain because one file for ~40 entries becomes a "does too much" file. Each domain file is independently readable and unit-testable.

Core types — types.ts

export type ToastTone = 'success' | 'error' | 'warning' | 'info';   // → colour
export type ToastIcon = 'check' | 'alert' | 'triangle' | 'info' | 'cross';

export type ToastDescriptor = {
type: ToastTone;
title: string;
message?: string;
duration?: number;
icon?: ToastIcon; // optional override; decouples icon from colour (Tick+Yellow)
} | null; // null = "No Toast Message"
  • Static entries are ToastDescriptor objects.
  • Dynamic entries are factories (params) => ToastDescriptor.
  • Inline (non-toast) strings (e.g. stakeError.min) are (params) => string.

Dispatch — notificationStore.ts

Add toast.show(d: ToastDescriptor):

  • d === null → no-op (this is how "No Toast Message" rows resolve).
  • else forward { type, title, message, duration, icon } to addNotification.

addNotification / Notification gains an optional icon?: ToastIcon. Existing toast.success/error/warning/info remain for back-compat during migration.

Renderer — Toast.tsx

  • iconMap keyed by ToastIcon. When a notification has an explicit icon, render that; otherwise fall back to the current type-derived icon.
  • Colour continues to come from type (bgColorMap).
  • This is the ONLY design-system change and enables Tick(check) + Yellow(warning).

Backend map absorbed — errors.ts + api.ts

  • Move ERROR_MESSAGES verbatim into messages.errors.byCode.
  • Move the HTTP status fallbacks into messages.errors.byHttpStatus.
  • parseApiError imports both from the catalog; resolution logic unchanged.
  • Single source of truth for all user-facing copy.

Message inventory (Phase 1 — from the Excel)

NC rows are omitted. Each change maps to a catalog key + the current call site. type column reflects Excel Icon/Colour: Tick+Green=success, !+Yellow=warning, Cross+Red=error, blue=info.

auth.ts

KeyCall site (current)typetitlemessage
signInFailedGenericuseAuth.ts:213, :298errorSign In FailedAn unexpected error occurred. Please try again.

(Other auth rows are NC; useAuth.ts:303 already uses this string.)

placement.ts (InlineBetSlip.tsx)

KeyCall site (current)typetitlemessage
stakeError.min(limit)caption :67 / :391 (inline red text)Min stake is <limit>
stakeError.maxPerBet(limit):235 toasterror(TBD title — see Q1)Max stake per bet is <limit>
stakeError.maxMarket():235 toasterror(TBD title — see Q1)Market limit exceeded - try a lower stake
stakeError.other():235 toasterror(TBD title — see Q1)Invalid Stake
betAccepted(p) non-fancy/BF:286 toast.success('Bet Placed',…)warningBet Accepted - Awaiting Match<Back/Lay> "<selection>" @ "<matched_odds>"
betPlacedFancy(p) BM:284 toast.warning('Submitted',…)successBet Placed Successfully!<Back/Lay> "<selection>" @ "<matched_odds>"
placementPartial:293 toast.warning('Partial',…)null (no toast)
placementFailed:300, :305 toast.error('Bet Failed',…)error(TBD title — see Q1)Unexpected Error

lifecycle.ts

KeyCall site (current)typetitlemessage
betMatched(p) fullorderStatus.ts:137successBet Matched<Back/Lay> "<selection>" @ "<matched_odds>"
betPartiallyMatched(p)NEW (classify branch)warning + icon:'check'Bet Partially Matched (<pct>%)<Back/Lay> "<selection>" @ "<matched_odds>"
serverNotifAccepteduseWebSocket.ts:271-276null (no toast)
serverNotifDeclined:275errorCould Not Place BetUnexpected Error
serverNotifOther:275null (no toast)
betfairResettlement:252-255 toast.info(…)null (no toast)
settled.win/lose/push/voidnotificationStore.ts:115-131 via :234null (no toast)
betDeclinedLapsedCancelledorderStatus.ts classifyUNDECIDED (TJ: "study further") — leave current behaviour

cancel.ts (useOpenBets.ts)

KeyCall site (current)typetitlemessage
cancelPartial:108warningPartially CancelledBet could not be fully cancelled
cancelFailed:118errorCancel FailedPlease try again later.

(cancelFull :110 is NC.)

Dependencies (gate content, not format)

Dynamic factories betMatched, betAccepted, betPlacedFancy, betPartiallyMatched require: side (Back/Lay), selection, matched_odds, and pct (partial %). These may not be present in the current /orders/batch response or the order:status / WS payloads. Phase 1 includes tracing those payloads (read-only). If a field is missing, the catalog key is authored with the param it needs and the backend gap is filed separately — the frontend does NOT synthesize odds/selection (financial-integrity: no fabricated values).

Testing

One *.test.mjs per domain, mirroring orderStatus.test.mjs: assert each factory/entry produces the exact type/title/message/icon/null the Excel specifies. parseApiError tests updated to read from messages.errors.

Migration

  • Phase 1 (this spec): build infra (types, toast.show, Toast.tsx icon override, errors absorption) + populate the catalog for every Excel message + migrate those call sites. Verify no behavioural regressions on NC paths.
  • Phase 2 (follow-up spec): sweep remaining hardcoded toast.* call sites (BetSlipFooter, one-click grids, and any others found by grep) onto the catalog so no user-facing copy remains inline.

Resolved questions

  • Q1 — "NA" titles → inline red text. Inline stake errors (min, maxPerBet, maxMarket, other) and the inline placement failure render as red text inside the bet slip (like the existing min-stake caption), NOT as toasts. They are (params) => string entries, not ToastDescriptor. This removes the :235 and :300/:305 toasts from InlineBetSlip.tsx in favour of a caption. The "TBD title" cells above are therefore N/A (no title).
  • Q2 — settled notifications → remove toast only. Stop the transient toast for settled win/lose/push/void; KEEP the persistent Notification Center entry. Implementation must confirm the toast and the Notification Center read from different sources before removing (see Implementation note).
  • Q3 — betDeclinedLapsedCancelled → unchanged. Leave classifyOrderStatusToast's current declined/lapsed/cancelled output as-is.

Implementation note (Q2): verify whether ToastContainer and NotificationCenter both read useNotificationStore or whether the Center reads the ['notifications'] backend query. If they share the store, removing the toast necessarily removes the Center entry — reconcile before implementing.

Cross-system impact

  • backend: possibly — only if matched_odds/selection/pct are not already in the WS/API payloads (tracked as a separate change).
  • frontend: the catalog + Toast.tsx icon override + call-site migrations.
  • services / infra: none.