Gamification Module
O template integra o SDK @cactus-agents/gamification com Smartico como provider de gamificação. A abordagem é híbrida: UI nativa (React) para listagens e detalhes, widgets overlay do Smartico para interações que não justificam UI própria.
Configuração
O domínio tem três arquivos em app/config/gamification/ — todos brand-overridable e importados sem suffix .config:
| Arquivo | Import | Papel |
|---|---|---|
gamification.ts | ~/config/gamification/gamification | Config principal client-side (gamificationConfig) |
gamification.server.ts | ~/config/gamification/gamification.server | smarticoHashConfig (fallback da saltKey) — server-only |
gamification-ui.ts | ~/config/gamification/gamification-ui | Knobs visuais (gamificationUiConfig) |
gamification.ts
import type { GamificationConfig } from "@cactus-agents/gamification";
export const gamificationConfig: GamificationConfig = {
enabled: true,
provider: "smartico",
smartico: {
libraryUrl: "/proxy/smartico.js",
labelKey: "",
brandKey: "",
visitorLabelKey: "",
visitorBrandKey: "",
allowLocalhost: true,
// Precisa ficar `false` em prod — ligado emite 70+ console calls por
// render do SmarticoInitializer e inunda a Observability da Cloudflare.
enableDebug: false,
allowPush: false,
},
naming: {
programName: "ClubVip",
coinsName: "VipCoins",
},
modules: {
missions: { enabled: true, native: true, public: true },
tournaments: { enabled: true, native: true, public: true },
store: { enabled: true, native: true },
miniGames: { enabled: true, native: true, showInactive: true },
levels: { enabled: true, native: true },
profile: { enabled: true, native: true },
notifications: { enabled: true, native: true },
badges: { enabled: false, native: true },
bonuses: { enabled: false, native: true },
jackpots: { enabled: false, native: true },
raffles: { enabled: false, native: true },
},
};
smartico.exclusiveTournaments?: string[] (opcional) lista IDs de torneios tratados como exclusivos: eles saem da lista pública e só aparecem quando a URL traz ?exclusive, ?premium, ?telegram ou ?whatsapp.
Hooks de config
app/hooks/useGamificationConfig.ts expõe três símbolos:
import {
resolveGamificationConfig, // função pura — usável no server
useGamificationConfig,
useGamificationEnabled,
} from "~/hooks/useGamificationConfig";
const config = useGamificationConfig();
const enabled = useGamificationEnabled();
resolveGamificationConfig() verifica se ao menos uma das keys obrigatórias do Smartico está preenchida (labelKey ou visitorLabelKey). Se nenhuma estiver, enabled é forçado para false — impedindo que o SDK carregue e que a UI renderize elementos de gamificação.
gamification.server.ts
Configuração server-side para geração do hash de autenticação:
import type { HashConfig } from "@cactus-agents/gamification";
export const smarticoHashConfig: HashConfig = {
saltKey: "",
};
A saltKey é um segredo server-side e não deve ser commitada nos arquivos de override. Use a variável de ambiente SMARTICO_SALT_KEY (secret do worker).
Prioridade de resolução da saltKey (app/services/smartico.server.ts):
const saltKey =
cloudflareEnv?.SMARTICO_SALT_KEY ?? process.env.SMARTICO_SALT_KEY ?? smarticoHashConfig.saltKey;
SMARTICO_SALT_KEYvia env var da Cloudflare (secret)SMARTICO_SALT_KEYviaprocess.env(dev local /.dev.vars)smarticoHashConfig.saltKeydo config file (fallback — normalmente vazio)
gamification-ui.ts
Knobs puramente visuais, default = look legado em toda superfície:
export const gamificationUiConfig: GamificationUiConfig = {
tournamentDetailLayout: "classic",
filtersStyle: "legacy",
};
Brands opt-in no redesign sombreando o arquivo em overrides/<brand>/app/config/gamification/gamification-ui.ts.
Overrides por brand
overrides/<brand-key>/app/config/gamification/gamification.ts
overrides/<brand-key>/app/config/gamification/gamification.server.ts
overrides/<brand-key>/app/config/gamification/gamification-ui.ts
<brand-key> vem de ORIGIN_DOMAIN normalizado. Override é substituição do arquivo inteiro — ver Override Files.
Inicialização
SmarticoInitializer é montado no layout (lazy) e gerencia todo o ciclo de vida:
SmarticoInitializer
├── 1. Carrega script Smartico dinamicamente (deferido em rotas quentes:
│ home, cassino, cassino ao vivo, sports, central de ajuda)
├── 2. Inicializa SDK com labelKey/brandKey
├── 3. Aguarda sdkReady + authHydrated (do useAccountsStore)
├── 4a. Autenticado:
│ ├── Identifica via smarticoHash lido do useSmarticoHashStore
│ ├── Carrega: profile, missions, badges, tournaments,
│ │ storeItems, miniGames, levels, currentLevel,
│ │ bonuses, jackpots, raffles, inboxUnread, translations
│ └── Aplica resolveTranslations (i18n Smartico)
└── 4b. Visitante:
├── Inicializa via vapi()
├── Carrega: missions, tournaments, storeItems, miniGames,
│ levels, jackpots, raffles, translations
└── Aplica resolveTranslations
O estado de auth vem de useAccountsStore (@cactus-agents/accounts/react), não de loaderData.
O hash do Smartico — cookie gm_id + store
:::danger O hash não trafega mais por loaderData
O doc SSR é auth-agnóstico (spec user-data-out-of-ssr, 2026-07): o loader do _layout não retorna auth nem smarticoHash. Um snippet que faça generateSmarticoUserHash(String(auth.user.id), cf) dentro do loader do _layout não compila — auth não existe lá.
Nunca leia auth de useRouteLoaderData("routes/_layout"). O gate canônico é useAuthGate() (app/hooks/useAuthGate.ts); ver State Management.
:::
O caminho real:
- Login (server).
app/utils/cookie.server.tsgrava o cookie não-HttpOnlygm_id=<userId>|<md5>:<expiry_ms>, TTL 24h — casado com o expiry embutido no hash. - Boot (client).
useAuthInit(app/hooks/useAuthInit.ts) chamagetGamificationMeta()de~/utils/cookie.client.ts. Essa função valida o expiry embutido com uma margem de 5 min e devolvenullquando o hash está ausente, malformado ou perto de expirar. Com hash válido, ela alimentauseSmarticoHashStore.setHash()— sem esperar o RTT do profile. - Fallback / SPA shell. Quando
getGamificationMeta()devolvenull, ouseAuthInitcai noGET /api/auth/profile, que gera um hash novo server-side (generateSmarticoUserHash) e o retorna no envelope. O hook grava no store e reescreve o cookie no client viasetGamificationMeta(). - Login/logout dentro da sessão SPA. Um segundo effect do
useAuthInitreage: limpa o hash no logout, refaz o fetch no login. - Consumo.
SmarticoInitializerlêuseSmarticoHashStore((s) => s.hash)direto, sem prop nenhuma. - Logout.
app/utils/cache-purge.client.tsremove ogm_id(é user-scoped e tem que sumir).
app/store/smartico.ts é a única fonte do hash no client. É SSR-safe (zustand puro, sem acesso a browser no top-level), mas nunca lido no server.
Módulos (enabled / native / public / showInactive)
O type é GamificationModuleConfig (@cactus-agents/gamification):
| Campo | Efeito |
|---|---|
enabled: false | Módulo completamente desativado (sem botão, sem página) |
enabled: true, native: true | UI React própria (páginas de gamificação, modais) |
enabled: true, native: false | Widget overlay do Smartico |
public | Visível para visitante não autenticado. Default: true |
showInactive | Mostra itens inativos/indisponíveis nas listagens. Default: true |
Comportamento na sidebar e no user menu
native: true→ o botão navega viarouteHref("gamification.missions"),routeHref("gamification.tournaments"), etc.native: false→ o botão abre o widget do Smartico diretamente.
Modo visitante
Páginas de listagem funcionam sem login (dados via vapi) para os módulos com public: true — hoje missions e tournaments no base. Ao tentar abrir o detalhe de uma missão ou torneio, redireciona para o modal de login.
Rotas
Os paths abaixo são os defaults — configuráveis via Route Registry (~/config/routes/paths). Use routeHref() de ~/utils/routes para gerar links.
| Chave | Path padrão | Descrição |
|---|---|---|
gamification | /vip | Hub de gamificação |
gamification.missions | /vip/missions | Listagem de missões com filtros |
gamification.tournaments | /vip/tournaments | Listagem de torneios com filtros |
gamification.tournament | /vip/tournaments/:id | Detalhe de torneio (leaderboard, prêmios) |
gamification.store | /vip/store | Loja com filtros |
gamification.miniGames | /vip/mini-games | Listagem de mini-games |
gamification.levels | /vip/levels | Timeline de progresso com perfil lateral |
gamification.badges | /vip/badges | Listagem de badges |
gamification.bonuses | /vip/bonuses | Listagem de bônus |
user.notifications | /user/notifications | Inbox nativo (mensagens expandidas, marcar lidas) |
user.rewards | /user/rewards | Recompensas dentro da área do usuário |
As rotas /vip/* vivem sob o layout routes/vip.tsx (auth-guarded) na árvore de rotas app/router/routes.ts — não existe mais um routes.config.ts. Há também um sitemap-vip.xml (ver SEO).
Componentes
Lista viva — consulte app/components/gamification/.
Layout
SmarticoInitializer— inicialização do SDK (montado no layout, lazy)GamificationGuard— guard das páginas/vip/*HeaderUserArea/UserPanelGamification— perfil, avatar, coins, nível
Sections
GamificationSection (genérica: título + grid + skeleton + empty state), GamificationFilters (tabs + busca), e uma por módulo: MissionsSection, TournamentsSection, MiniGamesSection, StoreSection, LevelsSection, BadgesSection, BonusesSection, JackpotsSection, RafflesSection, InboxSection.
Cards
app/components/gamification/cards/ — MissionCard, TournamentCard, MiniGameCard, StoreItemCard, BadgeCard, BonusCard, JackpotCard, RaffleCard, LevelCard, além de variantes (*Stacked, *Showcase, *Panel) resolvidas pelos registries mission-card-registry.ts e tournament-card-registry.ts, e MissionCountdownBadge.
Modais
MissionDetailModal— detalhe de missão com opt-in
Habilitando/desabilitando
Para desativar um módulo, altere o flag no app/config/gamification/gamification.ts (ou no override da brand):
modules: {
missions: { enabled: true, native: true, public: true },
tournaments: { enabled: false }, // desativado
// ...
}
:::warning Override é substituição de arquivo
Ao adicionar um campo novo ao gamificationConfig, propague para todos os overrides existentes. Sem isso a brand recebe undefined naquele campo.
:::