Pular para o conteúdo principal

State Management

O estado client-side do template vem de duas fontes, com donos diferentes:

FonteDonoO que guarda
useAccountsStore (@cactus-agents/accounts/react)CoreAuth (user, userInfo, isAuthenticated, authHydrated), wallet (wallet, transações), coins
Stores Zustand em app/store/BaseEstado de UI, de sessão e de feature — 28 módulos hoje

Dados de server (brand, clientEnv) chegam por loader + React Context, não por Zustand.

:::danger @cactus-agents/auth, /user e /wallet não existem mais Foram unificados em @cactus-agents/accounts. As stores app/store/{auth,brand,wallet,user,accountMenu}.ts também foram removidas. Qualquer código ou doc que use useAuthStore(), useWalletStore() ou useAccountMenuStore() está desatualizado. :::

Estado de auth/wallet — useAccountsStore

import { useAccountsStore } from "@cactus-agents/accounts/react";

const isAuthenticated = useAccountsStore((s) => s.isAuthenticated);
const user = useAccountsStore((s) => s.user);
const wallet = useAccountsStore((s) => s.wallet);
const refetchWallet = useAccountsStore((s) => s.refetchWallet);

Campos e ações que importam no dia a dia do base:

SliceCamposAções
Authuser, userInfo, isAuthenticated, authHydrated, lastProfileFreshAtsetAuthUser, clearAuth, setAuthHydrated, refreshAuthProfile
Walletwallet, isLoading, error, lastWalletFreshAtrefetchWallet({ force }), transferBonus, transferCashback
Transaçõestransactions, transactionsRaw, transactionsMeta, transactionsLoading, transactionsFilterfetchTransactions(filter)
Coins (dual-currency)activeCoinId, coinBalancessetActiveCoin, setCoinBalances

Dois pontos operacionais:

  • authHydrated fica true quando o primeiro fetch de perfil termina — logado ou não. É o sinal de "já sei o estado real", não de "está logado".
  • refetchWallet tem memoize por TTL de 2s; mutações que mexem no saldo (depósito, saque, aposta, transferência de bônus, resgate de cashback) precisam passar { force: true }.

Auth reactivity

:::danger NUNCA leia auth de useRouteLoaderData("routes/_layout") O campo auth não existe mais nesse loader (spec user-data-out-of-ssr, 2026-07): o documento SSR é auth-agnóstico. Ler o loader do layout para decidir visibilidade é um snapshot morto — não atualiza em login/logout via modal. :::

O gate canônico é useAuthGate() (app/hooks/useAuthGate.ts):

import { useAuthGate } from "~/hooks/useAuthGate";

const isLogged = useAuthGate();

Como ele se comporta, em ordem cronológica:

  1. SSR e primeiro render clientfalse. Toda página nasce com a casca de guest.
  2. Pre-paint → um script inline síncrono em root.tsx lê o cookie não-HttpOnly is_authenticated=1 e marca data-auth-flag no <html> antes do primeiro paint. Isso segura o flash de header guest → logado via CSS, sem esperar JS de aplicação.
  3. Pós-mount → o hook lê hasAuthFlagCookie() (o mesmo cookie) como sinal otimista.
  4. Pós-authHydrated → a store reativa assume: storeAuthenticated || cookieAuth. O || cobre a janela do fetch de perfil no caminho de casca SPA.

O hook também escuta o evento AUTH_FLAG_CLEARED_EVENT (~/utils/cookie.client) para reagir na hora quando o cookie de flag é limpo.

Para código que precisa do usuário em si (não só do booleano), leia direto de useAccountsStore — é reativo pelos mesmos motivos.

Stores do base (app/store/)

28 módulos hoje. Sem dumps de interface aqui de propósito: elas mudam toda semana e a fonte de verdade é o arquivo.

Sessão, layout e overlays

StoreHookPapel
layout.tsuseLayoutStoreNavegação estrutural: mobileMenuOpen, sidebarOpen, userPanelOpen, hideBottomNav, sectionToggles (seeded do config da brand — uma seção começa aberta a menos que declare defaultCollapsed: true) e syncSectionsForRoute
side-sheets.tsuseSideSheetsStoreQual side-sheet está aberto (favorites | recents | notifications). Só um por vez — abrir um substitui o anterior sem estado fechado intermediário
floatingStack.tsuseFloatingStackStoreOrdenação vertical da coluna flutuante no canto inferior direito
topbar.tsuseTopbarStoreTopbar de notificações: fila da brand, avaliação de shouldShow, dismiss com persistência em cookie/sessionStorage
bottomNotification.tsuseBottomNotificationStoreMesma mecânica do topbar, para a notificação flutuante do rodapé (inclui rotação e revalidate)
installTooltip.tsuseInstallTooltipStoreTooltip de instalação de app (variantes por plataforma, ex. ios_safari)
sportsNav.tsuseSportsNavStorePath atual dentro do SDK do sportsbook (o SDK navega por history.pushState, que o React Router não enxerga) + controles do SDK

Contrato de ordenação de overlays

floatingStack.ts orquestra a pilha flutuante para que nenhum widget precise de cálculo de pixel manual. A ordem é declarada em FLOATING_LAYERS (índice 0 = mais embaixo):

installTooltip → updateBanner → bottomNotification → lastGame → campaign → backToTop

Adicionar uma camada nova exige quatro passos: a chave em FLOATING_LAYERS na posição vertical correta, a altura em LAYER_HEIGHTS, useRegisterFloating(layer, isVisible) no widget e useFloatingBottom(layer) para aplicar o offset. Um widget que pode encolher numa aba de borda (FloatingEdgeTab) permanece na mesma coluna: re-registra com altura menor e região "right".

side-sheets.ts coordena com useLayoutStore.userPanelOpen dentro do SideSheetRoot — abrir um sheet fecha o painel do usuário e vice-versa. A dependência é unidirecional de propósito: a store de sheets conhece layout.ts, nunca o contrário.

Catálogo, jogos e busca

StoreHookPapel
games.tsuseGamesStoreStore central do cassino: homeRows, categories, providers, topWins, lastWins, hubChips, caches (gameStatsCache, gamesBySlug) e o slice de favoritos (favoriteSlugs, _isFavoritesHydrated, pendingFavoriteWrites)
search.tsuseSearchStoreBusca compartilhada entre o input do header e o da página. Debounce de 300ms, abort do fetch anterior, lastResolvedTerm (política "a URL espelha o resultado, não o buffer de digitação") e persistência em sessionStorage
pre-game.tsusePreGameStoreJogo do bottom-sheet de pre-game (aberto por tap no card, mobile)
gameAssistant.tsuseGameAssistantStoreAssistente flutuante in-game: expanded + qual sheet está aberto

Auth, validação e KYC (UI)

StoreHookPapel
authModal.tsuseAuthModalStoreQual modal de auth está aberto + authModalReason (hoje só session_expired, produzido por handleUnauthorized após um 401)
recovery.tsuseRecoveryStoreFluxo do modal de recuperação de senha (steps, token temporário, e-mail/telefone mascarados, disponibilidade de KYC). Dados sensíveis transitam só server-side via cookie HttpOnly
validationSteps.tsuseValidationStepsStoreModal de validações contextuais (deposit, withdraw, …). Tem um contador de abertura usado como key para forçar remontagem quando o modal reabre no mesmo tick
validationRuntime.tsuseValidationRuntimeStoreResultado global de fetchAllValidations()
passwordValidation.tsusePasswordValidationStoreModal standalone de confirmação de senha
kyc.tsuseKycStoreFluxo completo de KYC. O mode discrimina iframe (BFF devolveu url) de sdk (devolveu token, painel por operador — Unico, Sumsub). Ver KYC
onboarding.tsuseOnboardingStoreSinal efêmero "acabou de registrar", consumido por useAfterRegisterFlow para abrir o modal de depósito (esperando o LimitsStep quando o caminho regulatório está ativo)
protection.tsuseProtectionStoreCoordenação de accordions da página /user/protection (um painel pode forçar a abertura de vizinhos)

Financeiro e recompensas

StoreHookPapel
payments.tsusePaymentsStoreModal de depósito/saque, providers, método selecionado, cupom, valor inicial e os flags de auto-open/auto-submit
walletModal.tsuseWalletModalStoreAbre/fecha o modal de carteira (o dado da carteira vive em useAccountsStore)
rewards.tsuseRewardsStorePágina /user/rewards: abas, filtros, paginação de disponíveis/finalizados, modal de resgate
rewardsCount.tsuseRewardsCountStoreContador app-wide de prêmios resgatáveis (a rota de prêmios reseta a store ao desmontar, então o número precisa de casa própria)
ftdCheckin.tsuseFtdCheckinStoreEstado do check-in pós-FTD (missões, dia, ftdDate, task)

Gamificação, campanhas e cache de sessão

StoreHookPapel
gamification.tsuseGamificationStoreEstado do SDK Smartico: ready, sdkIdentified, visitorMode, perfil, missões, badges, torneios, loja, mini-games, níveis, bônus, jackpots, sorteios, inbox e traduções
smartico.tsuseSmarticoHashStoreO smarticoHash resolvido no client. Não vem mais do loader (doc SSR auth-agnóstico) — é alimentado pelo cookie gm_id
strategy.tsuseStrategyStoreOverlay de campanha/estratégia (iframe + dedup por sessão: uma estratégia com repeat=false não reabre no resto da aba)
userConfigCache.tsuseUserConfigCacheStoreCache de sessão da área do usuário (contas sociais, anos do informe de rendimentos) para as abas não re-buscarem com skeleton a cada mount. null = nunca buscado nesta sessão

Dados de server (loaders)

Brand e clientEnv chegam pelo loader do _layout.tsx e são distribuídos por Context, não por Zustand:

// app/routes/_layout.tsx — shape real
interface LayoutLoaderData {
brand: BrandConfig | null;
brandError: BrandError | null;
clientEnv: ClientEnv;
serverIsMobile: boolean;
legalTerms: Promise<Pick<LegalTerm, "route" | "title">[]>; // deferred
orgCountry: { alpha2: string; name: string } | null;
}

Não há auth nem smarticoHash aqui. Existe um teste leak-canary (app/routes/__tests__/no-user-data-in-ssr.test.ts) que falha se qualquer campo derivado do usuário logado for reintroduzido. Ver Routing.

Padrão de acesso

Precisa deUse
"Está logado?" para UIuseAuthGate()
Usuário / perfil / carteirauseAccountsStore((s) => …)
Brand configuseBrand() (Context)
Env do clientuseClientEnv() / useCassinoMode()
Navegação estruturaluseLayoutStore()
Catálogo / favoritosuseGamesStore(), useFavorites(), useIsFavorite(slug)
Modaisa store dedicada de cada um (qualquer componente pode abrir/fechar)
Widget flutuante novouseRegisterFloating() + useFloatingBottom() do floatingStack