Pular para o conteúdo principal

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:

ArquivoImportPapel
gamification.ts~/config/gamification/gamificationConfig principal client-side (gamificationConfig)
gamification.server.ts~/config/gamification/gamification.serversmarticoHashConfig (fallback da saltKey) — server-only
gamification-ui.ts~/config/gamification/gamification-uiKnobs 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: "",
};
cuidado

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;
  1. SMARTICO_SALT_KEY via env var da Cloudflare (secret)
  2. SMARTICO_SALT_KEY via process.env (dev local / .dev.vars)
  3. smarticoHashConfig.saltKey do 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.

:::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:

  1. Login (server). app/utils/cookie.server.ts grava o cookie não-HttpOnly gm_id=<userId>|<md5>:<expiry_ms>, TTL 24h — casado com o expiry embutido no hash.
  2. Boot (client). useAuthInit (app/hooks/useAuthInit.ts) chama getGamificationMeta() de ~/utils/cookie.client.ts. Essa função valida o expiry embutido com uma margem de 5 min e devolve null quando o hash está ausente, malformado ou perto de expirar. Com hash válido, ela alimenta useSmarticoHashStore.setHash() — sem esperar o RTT do profile.
  3. Fallback / SPA shell. Quando getGamificationMeta() devolve null, o useAuthInit cai no GET /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 via setGamificationMeta().
  4. Login/logout dentro da sessão SPA. Um segundo effect do useAuthInit reage: limpa o hash no logout, refaz o fetch no login.
  5. Consumo. SmarticoInitializeruseSmarticoHashStore((s) => s.hash) direto, sem prop nenhuma.
  6. Logout. app/utils/cache-purge.client.ts remove o gm_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):

CampoEfeito
enabled: falseMódulo completamente desativado (sem botão, sem página)
enabled: true, native: trueUI React própria (páginas de gamificação, modais)
enabled: true, native: falseWidget overlay do Smartico
publicVisível para visitante não autenticado. Default: true
showInactiveMostra itens inativos/indisponíveis nas listagens. Default: true

Comportamento na sidebar e no user menu

  • native: true → o botão navega via routeHref("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.

ChavePath padrãoDescrição
gamification/vipHub de gamificação
gamification.missions/vip/missionsListagem de missões com filtros
gamification.tournaments/vip/tournamentsListagem de torneios com filtros
gamification.tournament/vip/tournaments/:idDetalhe de torneio (leaderboard, prêmios)
gamification.store/vip/storeLoja com filtros
gamification.miniGames/vip/mini-gamesListagem de mini-games
gamification.levels/vip/levelsTimeline de progresso com perfil lateral
gamification.badges/vip/badgesListagem de badges
gamification.bonuses/vip/bonusesListagem de bônus
user.notifications/user/notificationsInbox nativo (mensagens expandidas, marcar lidas)
user.rewards/user/rewardsRecompensas 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. :::