Sports
Integracao de sportsbook multi-provider no template. Os providers suportados sao First (iframe), Altenar (JS SDK), Betby (JS SDK) e Rogue (sportsbook nativo, SDK React) — a uniao vive no type SportsProvider de @cactus-agents/sports:
const PROVIDERS: readonly SportsProvider[] = ["first", "altenar", "betby", "rogue"];
:::info @cactus-agents/sports-rogue nao vive no core
O SDK do Rogue e a dependencia @cactus-agents/sports-rogue (ver a versao em front-web-base/package.json). Ela usa o scope npm @cactus-agents/* mas nao e publicada a partir de front-cactus-core — nao procure um packages/sports-rogue la. @cactus-agents/sports (esse sim do core) segue sendo o pacote de tipos/config/roteamento de provider.
:::
Visao geral
app/config/sports/sports.ts (dados estaticos, brand-overridable)
|
+-- app/config/sports/provider.ts (override por AMBIENTE: FORCE_SPORTBOOK)
|
+---------+-----------+
| |
v v
useSportsConfig() sportsConfig (import direto) <-- client / server
| |
v v
_layout.tsx loaders / API routes
|
v
SportsIframeFirst / SportsAltenar / SportsBetby / SportsRogue
A selecao do provider e feita 100% por configuracao — sem mudancas de codigo para trocar entre providers.
:::info Regra arquitetural
app/config contem somente dados estaticos de brand. Hooks, parsers e logica de negocio nao ficam em app/config. O hook useSportsConfig() vive em app/hooks/useSportsConfig.ts.
Excecao deliberada: app/config/sports/provider.ts guarda helpers puros (resolveForceSportbook, applyForceSportbook) que nao leem env — quem le e o caller (client via useClientEnv, server via context.cloudflare.env).
:::
Configuracao
app/config/sports/sports.ts
Arquivo principal (brand-overridable). Import: ~/config/sports/sports — sem suffix .config.
import type { SportsModuleConfig } from "@cactus-agents/sports";
export const sportsConfig: SportsModuleConfig = {
main: null, // provider principal (/sports) — null = desabilitado
test: null, // provider de teste (/sports-test) — null = desabilitado
first: {
// Default: o First assume a UX mobile — bottom nav e footer somem do
// lado do site e o footer e encaminhado pro iframe via postMessage.
hideBottomNav: true,
hideFooter: true,
},
altenar: {
integration: "brandName",
libraryUrl: "https://sb2wsdk-altenar2-stage.biahosted.com/altenarWSDK.js",
},
betby: {
brandId: "00000000000000000000",
theme: "default",
libraryUrl: "https://ui.invisiblesport.com/bt-renderer.min.js",
},
rogue: {
theme: "default",
scheme: "dark",
oddsStyle: "decimal",
},
};
O sportsConfig e importado diretamente tanto no client quanto no server — nao existe um wrapper sports.server. As API routes fazem import { sportsConfig } from "~/config/sports/sports".
Override por brand
overrides/<brand-key>/app/config/sports/sports.ts
<brand-key> vem de ORIGIN_DOMAIN normalizado. Como todo override, a substituicao e do arquivo inteiro (sem deep-merge) — ver Override Files.
Hoje todas as brands do base declaram main: "first".
Override por ambiente — FORCE_SPORTBOOK
app/config/sports/provider.ts implementa um override em runtime, por ambiente, com prioridade sobre o main do arquivo:
export function resolveForceSportbook(raw?: string | null): SportsProvider | null;
export function applyForceSportbook(
config: SportsModuleConfig,
forced: SportsProvider | null,
): SportsModuleConfig;
- A env
FORCE_SPORTBOOKrecebe o nome do provider ("first" | "altenar" | "betby" | "rogue"). - Valor vazio, ausente ou invalido →
null(usa o arquivo). - So o
maine afetado; otestfica inalterado.
Pra que serve: rodar um ambiente dedicado (ex. sports-7k-bet-br…) com um provider diferente do de producao, na mesma brand / mesmo ORIGIN_DOMAIN, sem tocar no override. E o caminho usado hoje pra exercitar o Rogue: prod da 7k segue em First, e o ambiente dedicado seta FORCE_SPORTBOOK=rogue. Em dev local, FORCE_SPORTBOOK=rogue no .dev.vars tem o mesmo efeito.
SPORTS_ROGUE_API_KEY
Chave enviada como header x-api-key nas rotas de token do sportbook (/cactus-sportbook/auth/*) do BFF. Server-side only — nunca exposta ao client, setada como secret do worker (nao em [vars]). Necessaria quando o provider ativo e o Rogue.
Hook — useSportsConfig()
Hook central consumido por rotas e componentes (app/hooks/useSportsConfig.ts). Ele importa sportsConfig de ~/config/sports/sports, aplica o FORCE_SPORTBOOK e resolve o provider ativo via getActiveProvider() do SDK — entao todos os consumidores veem a config efetiva.
import { useSportsConfig } from "~/hooks/useSportsConfig";
const {
config, // SportsModuleConfig EFETIVO (com FORCE_SPORTBOOK aplicado)
main, // provider principal efetivo
test, // provider de teste
activeProvider, // provider ativo para a rota atual
hasSports, // true se main != null
hasTestRoute, // true se test !== null
isTestRoute, // true na rota de teste
getConfigFor, // sub-config de um provider
firstHideBottomNav, // flags do First com default seguro (undefined === true)
firstHideFooter,
} = useSportsConfig();
Componentes
SportsIframeFirst
Renderiza <iframe> do First com comunicacao postMessage. Sincroniza navegacao bidirecional entre o iframe e o React Router via URLs semanticas.
SportsAltenar
Carrega o SDK Altenar dinamicamente, inicializa com AltenarWSDK.init() e sincroniza navegacao via onRouteChangeDetailed. Busca token de auth em /api/sports/altenar-token.
SportsBetby
Carrega o SDK Betby (BTRenderer) e autentica via /api/sports/betby-jwt. Recarrega a pagina em mudancas de auth (login/logout) para reinicializar o SDK.
SportsRogue
Monta o SDK React nativo (@cactus-agents/sports-rogue) via createSportsbook, passando callbacks de auth/login/deposito, locale, tema, etc. O SDK renderiza o sportsbook in-app (Shadow DOM) consumindo a Rogue API da First. Fica em app/components/sports/SportsRogue.tsx.
Campos host/env-derivados (locale, currency, logo do ticket, logoBaseUrl/assetBaseUrl) nao ficam na config da brand — o SportsRogue.tsx os resolve do brand/env.
Rotas
| Chave | Path padrao | Descricao |
|---|---|---|
sports | /sports | Layout + index. Renderiza o provider ativo. |
sports.catchAll | /sports/* | Catch-all. Deep links de esporte. |
sports.test | /sports-test | Provider alternativo para testes. |
search.sports | /search/sports | Busca esportiva (pagina propria) |
Paths sao configuraveis via Route Registry (~/config/routes/paths); use routeHref() / sportPath() de ~/utils/routes.
O layout (routes/sports/_layout.tsx) monta o componente do provider uma vez e mantem vivo em todas as sub-rotas. O conteudo interno e controlado pelo SDK do provider, nao por rotas React.
API Routes
| Rota | Descricao |
|---|---|
api/sports/search | Busca esportiva |
api/sports/launch | Launch First (autenticado) |
api/sports/anonymous-launch | Launch First (anonimo) |
api/sports/betby-jwt | JWT para Betby |
api/sports/altenar-token | Token para Altenar |
api/sports/rogue-anonymous | Token anonimo da Rogue API |
api/sports/rogue-login | Token logado da Rogue API (cookie de sessao) |
api/sports/rogue-proxy/* | Proxy same-origin para a Rogue API real, com streaming de SSE |
Sidebar — app/config/layout/sidebar-sports.ts
Define os items de navegacao com paths por provider. Import: ~/config/layout/sidebar-sports.
Exports:
| Export | Conteudo |
|---|---|
sportsMainItems | Items da secao "Esportes" (hoje 3: live, upcoming, e-sports) |
topSportsItems | Sub-secao "Top Sports" (hoje 5 esportes) |
popularChampionships | Secao "Popular" (hoje 12 campeonatos) |
resolveMenuItemPath | Re-export de @cactus-agents/sports |
SportsSidebarItem | Type — estende SportsSidebarItemBase com icon, i18nKey, borderGradient |
export const sportsMainItems: SportsSidebarItem[] = [
{
slug: "live",
i18nKey: "layout:sidebar.live_sports",
label: "Live",
section: "main",
firstPath: sportPath("/live"),
betbyPath: sportPath("/live"),
altenarPath: sportPath("/live"),
roguePath: sportPath("/"),
},
// ...
];
Cada item carrega firstPath, betbyPath, altenarPath e/ou roguePath; o path correto e resolvido em runtime por resolveMenuItemPath().
:::note roguePath cai na home do sportsbook
O esquema de URL do SDK Rogue enderece apenas sport/league/event por slug — nao ha URL de filtro "live"/"upcoming" nem um esporte unico "E-Sports". Por isso os sportsMainItems apontam pro root do sportsbook (sportPath("/")), que ja abre com o filtro live. Deep-links por slug pra Top Sports entram nos overrides por brand, porque os slugs sao locale-specific.
:::
A lista e brand-overridable (overrides/<brand>/app/config/layout/sidebar-sports.ts), e hoje a maioria das brands do base tem o proprio.
Configurando uma nova marca
- Criar
overrides/<brand-key>/app/config/sports/sports.ts - Definir
maincom"first","altenar","betby","rogue"ounull - Opcionalmente definir
testpara habilitar/sports-test - Configurar o bloco do provider escolhido no mesmo arquivo. Para
rogue, o override da7k-bet-brtraz o bloco completo e comentado (paleta + priorizacao de esportes/regioes/ligas/mercados) — use como referencia - Se o ambiente precisa de um provider diferente do
mainestatico, useFORCE_SPORTBOOKem vez de criar um override novo - Customizar items do sidebar em
overrides/<brand-key>/app/config/layout/sidebar-sports.tsse necessario