User Account
Area do usuario no template: dados pessoais, configuracoes, historico, protecao (responsible gaming), recompensas, indicacoes e informe de rendimentos.
:::danger @cactus-agents/user nao existe mais
Os pacotes @cactus-agents/auth, @cactus-agents/user e @cactus-agents/wallet foram deletados (core 452450e, 2026-04-17) e unificados em @cactus-agents/accounts. Nenhum import … from "@cactus-agents/user" resolve.
Regra pratica:
- Regras de negocio e constantes (
filterTimeoutOptions,TIMEOUT_DAYS_OPTIONS,isoDurationToHours,isAccountRestricted, …) →@cactus-agents/accounts - Estado e hooks React (
useAccountsStore,useProfile,useFormatMoney, …) →@cactus-agents/accounts/react
Ver SDK — accounts. :::
Estado de auth e perfil
Nao ha mais app/store/auth.ts nem app/services/user.client.ts. user, userInfo, isAuthenticated e authHydrated vivem em useAccountsStore:
import { useAccountsStore } from "@cactus-agents/accounts/react";
const user = useAccountsStore((s) => s.user);
const userInfo = useAccountsStore((s) => s.userInfo);
:::danger Nao leia auth do loader do _layout
O doc SSR e auth-agnostico (spec user-data-out-of-ssr, 2026-07): o loader do _layout nao retorna auth — o campo nao existe mais. useRouteLoaderData("routes/_layout") nunca deve ser usado pra decidir visibilidade ou comportamento de UI.
Pra "o usuario esta logado?" em UI, use o gate canonico useAuthGate() (app/hooks/useAuthGate.ts): SSR sempre pinta guest, o cookie nao-HttpOnly is_authenticated e o sinal otimista pos-mount, e a store reativa assume depois de authHydrated. Ver State Management.
:::
Rotas
A area do usuario foi refatorada para um shell de tabs (routes/user/_layout.tsx, auth-protegido). Paths sao defaults — configuraveis via Route Registry (~/config/routes/paths); use routeHref() de ~/utils/routes.
| Chave | Path padrao | Arquivo | Descricao |
|---|---|---|---|
user.wallet | /user/wallet | routes/user/wallet.tsx | Carteira — ver Wallet |
user.rewards | /user/rewards | routes/user/rewards.tsx | Recompensas resgataveis |
user.data | /user/data | routes/user/general.tsx | Dados pessoais + secao bancaria por pais |
user.history | /user/history | routes/user/history.tsx | Historico unificado (transacoes + login) |
user.config | /user/config | routes/user/config.tsx | Preferencias, contratos, marketing, seguranca |
user.refers | /user/refers | routes/user/refers.tsx | Indicacoes (app/config/referral/, useReferral()) |
user.incomeReport | /user/income-report | routes/user/income-report.tsx | Informe de rendimentos |
user.protection | /user/protection | routes/user/protection.tsx | Responsible gaming |
Fora do shell de tabs (full width):
| Chave | Path padrao | Arquivo |
|---|---|---|
user.notifications | /user/notifications | routes/user/notifications.tsx |
user.validate | /user/validate/:type/:token | routes/user/validate.$type.$token.tsx |
:::warning Chaves que nao existem
user.account, user.security, user.loginHistory, user.accountProtection e user.irpf nao sao chaves do registry. routeHref("user.account") e erro de typecheck. A pagina de seguranca virou secao dentro de user.config; historico de login virou um filtro de user.history; IRPF virou user.incomeReport.
:::
Tab bar — app/config/layout/user-tabs.ts
As tabs sao declarativas e brand-overridable:
export interface UserTab {
key: string;
labelKey: string;
icon: string;
routeKey: RouteKey;
/** Quando false (ou a funcao retorna false), a tab e escondida. */
condition?: boolean | ((brand: BrandConfig) => boolean);
}
UserTabNavigation (app/components/user/UserTabNavigation.tsx) filtra em tempo de render:
| Condicao | Efeito |
|---|---|
| conta restrita | esconde a tab referral |
featuresConfig.incomeReport falsy | esconde a tab incomeReport |
accountFeaturesConfig.protectionPageEnabled falsy | esconde a tab protection |
tab.condition resolve false | esconde a tab |
A tab referral tem condicao propria: quando featuresConfig.referralVariant e "default", ela so aparece se brand.settings.bonus.referral.active e o amount > 0.
Forks podem sobrescrever overrides/<brand>/app/config/layout/user-tabs.ts pra adicionar, remover ou reordenar tabs.
Painel flutuante do usuario
Alem das paginas, existe um painel (app/components/user/panel/) com abas Perfil / Dados / Notificacoes, montado por UserPanel e suas variantes (UserPanelFloatingDesktop, UserPanelFloatingTabs). Ele e config-driven por dois arquivos em app/config/widgets/:
| Config | Conteudo |
|---|---|
user-panel-data-menu.ts | Itens do menu "Dados" (userPanelDataMenuConfig) |
user-panel-shortcuts.ts | Atalhos da aba Perfil (userPanelShortcutsConfig) |
Ambos sao brand-overridable e substituidos por arquivo inteiro (sem deep-merge).
Feature flags da area do usuario
app/config/features/account.ts (accountFeaturesConfig, brand-overridable):
export const accountFeaturesConfig: AccountFeatureFlags = {
accountSetLimits: false,
accountTimeoutLimits: false,
pixRandomEnabled: true,
redeemableRewards: true,
userPreferences: false,
protectionPageEnabled: true,
};
:::warning Propague campos novos
Override e substituicao de arquivo inteiro. Ao adicionar um campo aqui, replique em todos os overrides/<brand>/app/config/features/account.ts — senao a brand recebe undefined.
:::
Componentes
Lista viva — consulte app/components/user/.
Dados pessoais (components/user/account/) — rota user.data
InfosSection— nome, data de nascimento, genero, usernameDocumentSection— documento (CPF, RUT, etc.)EmailSection— email com verificacaoPhoneSection— telefone com verificacao SMSAddressSection— endereco completo com estados (por pais)ChileIdentityValidationBlock— validacao RUT + DIDIT (so CHL comRUT_VALIDATION); ver KYCSectionWrapper— casca visual compartilhada
Secao bancaria (por pais)
Renderizada em routes/user/general.tsx condicionada por useCountry().payments.bankAccountSection:
| Valor | Componente | Pais |
|---|---|---|
"pix" | PixSection | BRA — chave Pix via /api/payments/bank-account |
"clabe" | ClabeSection | MEX — conta CLABE |
"chl-banks" | ChlBanksSection | CHL — conta bancaria generica |
"nga-banks" | NgaBanksSection | NGA — conta bancaria nigeriana |
null | (nenhum) | Outros paises |
Configuracoes (user.config)
PreferencesSection— preferencias do usuario (gated poraccountFeaturesConfig.userPreferences); ver User PreferencesContractsSection— termos aceitos, dinamico vialegal.requiredTermsdo country-configMarketingSection— preferencias de comunicacaoTwoFactorSection— ativar/desativar 2FA (SMS ou email)SocialAccountsSection— contas vinculadas; filtrada porfeatures.socialAuth.*(so exibe providers habilitados pra brand)PasswordSection— alterar senha
Protecao / Responsible Gaming (components/user/protection/)
A pagina (routes/user/protection.tsx) e controlada por feature flags vindas da API (/bff/features), lidas de brand.features:
| Feature Flag | Secoes visiveis |
|---|---|
features.accountSetLimits === true | DepositLimitSection, BetLimitSection, LossLimitSection, TimeLimitSection |
features.accountTimeoutLimits === true | TimeoutSection |
| — | SelfExclusionSection sempre presente no bloco "mais opcoes" |
Outros componentes do dominio: LimitAccordion (UI compartilhada, com indicador "Ativo"), ExclusionAlternatives (sugere pausa/limites antes da autoexclusao quando o motivo tem hasAlternative), RestrictedModeAlert, SelfExclusionSupportPrompt / SelfExclusionSupportChat (modo featuresConfig.selfExclusionViaSupport).
RestrictedModeHome cobre a home quando a conta esta restrita.
:::info Constantes e helpers de responsible gaming vem do core
TIMEOUT_DAYS_OPTIONS, SELF_EXCLUSION_MONTHS_OPTIONS, LIMIT_PERIOD, SPA_EXCLUSION_CODES e os helpers parseLimitPeriod, isoDurationToHours, hoursToIsoDuration, isPermanentExclusion, isLimitChangeBlocked, getLimitChangeUnblocksAt, getMinSelfExclusionMonths, getMinTimeoutDays, filterSelfExclusionOptions, filterTimeoutOptions, isAccountRestricted, getAccountRestriction, getRestrictionType — todos de @cactus-agents/accounts. O template nunca redefine essas regras.
:::
Extensao de restricoes ativas
Com uma autoexclusao ou pausa ativa, o usuario pode estender o periodo (duracao maior). Nao e possivel reduzir nem remover a restricao ativa.
| Estado | SelfExclusionSection | TimeoutSection |
|---|---|---|
| Inativo | Form completo — todas as opcoes | Form completo — todas as opcoes |
| Self-exclusion ativa (nao permanente) | Alerta + form com opcoes filtradas (>= minimo) | N/A |
| Pausa ativa | N/A | Alerta + form com opcoes filtradas (>= minimo) |
| Permanente | Alerta — form oculto | N/A |
| Operator exclusion | Alerta — form oculto | N/A |
| SPA exclusion | Alerta — form oculto | N/A |
| Pausa ativa sem opcoes disponiveis | N/A | Alerta + "periodo maximo atingido" |
A filtragem usa as funcoes puras do SDK (aceitam null/undefined com seguranca):
import { filterTimeoutOptions, TIMEOUT_DAYS_OPTIONS } from "@cactus-agents/accounts";
import { useAccountsStore } from "@cactus-agents/accounts/react";
const userInfo = useAccountsStore((s) => s.userInfo);
const availableTimeoutOptions = filterTimeoutOptions(userInfo);
import {
filterSelfExclusionOptions,
isPermanentExclusion,
SELF_EXCLUSION_MONTHS_OPTIONS,
} from "@cactus-agents/accounts";
const availableExclusionOptions = filterSelfExclusionOptions(userInfo);
O label do submit muda automaticamente: "Ativar" sem restricao ativa, "Estender" com restricao ativa.
:::caution Restricoes impostas externamente
Quando a restricao veio de operador ou da SPA (autoridade reguladora), o usuario nao pode modificar — apenas o alerta e exibido. A deteccao usa isAccountRestricted() / getAccountRestriction() / getRestrictionType() do SDK, e o hook useAccountRestriction() (app/hooks/useAccountRestriction.ts) empacota isso pra UI.
:::
Header / menu
HeaderUserArea— area do usuario logado (avatar, nome, saldo)UserBox— box compacto do usuarioUserSummaryHeader— cabecalho da area de usuarioUserTabNavigation— tab bar do shellUserPanel+components/user/panel/*— painel flutuante
Recompensas (components/user/rewards/)
RewardCard, RewardCardSkeleton, RewardsTabs, RewardsFilters, RewardsEmpty, RedeemModal — alimentados por useRewards() / useRewardsCountSync().
API Routes
Todas registradas em app/router/routes.ts. Ver a arvore la pra lista completa; as principais do dominio:
| Rota | Descricao |
|---|---|
api/auth/profile | Perfil completo do usuario (+ smarticoHash) |
api/user/update-profile | Atualiza dados do perfil |
api/user/update-address | Atualiza endereco |
api/user/update-pending-data | Dados pendentes (fluxo regulatorio) |
api/user/update-marketing | Preferencias de comunicacao |
api/user/update-preferences | Preferencias do usuario (whitelist no core) |
api/user/check-password | Verifica senha atual |
api/user/change-password | Altera senha |
api/user/add-phone | Adiciona telefone |
api/user/two-factor | 2FA |
api/user/social-accounts / api/user/social-accounts/:id | Contas sociais vinculadas |
api/user/login-history | Historico de logins |
api/user/update-limits | Limites de responsible gaming |
api/user/timeout-limits | Pausa temporaria |
api/user/self-exclusion | Autoexclusao |
api/user/store-document | Upload/registro de documento |
api/user/documents/verify-identity | Passo 3 da identidade CHL |
api/user/last-casino-games | Ultimos jogos jogados |
api/user/referrals / api/user/indication-stats / api/user/referral-indicator | Indicacoes |
api/user/zendesk-token | Token do Zendesk |
api/payments/bank-account | Conta bancaria (PIX / CLABE / CL / NG) |
:::note Padrao de proxy
Rotas api/* sao proxies server-side: leem o JWT do cookie HttpOnly, delegam ao service do SDK e retornam dados ja transformados. Erros usam proxyErrorResponse() + unauthorizedNoToken(). Nenhuma logica de negocio no route handler. Ver Services.
:::
Informe de rendimentos (user.incomeReport)
A tab e escondida quando featuresConfig.incomeReport e falsy — nao ha <Navigate> com routeHref("user.account") (essa chave nao existe).
O fluxo em routes/user/income-report.tsx:
- O ano disponivel vem de
useUserConfigCacheStore(incomeReportYears);incomeReportAccessDeniedcobre o caso de acesso negado - O usuario escolhe ano e formato (download ou email)
- A request gera o informe no backend
- Polling local (
pollingStatus:idle→processing→success|error, com timeout) acompanha a geracao - Download do PDF ou confirmacao de envio por email
Contratos por pais
ContractsSection usa legal.requiredTerms do country-config:
const { legal } = useCountry();
// legal.requiredTerms: ["tc", "privacy", "lgpd", "law"] para BRA
// ["tc", "privacy"] para CHL
Nao existe if (country === "BRA") no componente — ele usa o que o SDK define pro pais configurado. Quando features.userMigration === true, o termo "migrate" e adicionado automaticamente.