FTD Suite — Cashback (D0), Check-in (D1) e Oferta
Tres subsistemas de retencao ligados ao FTD (first time deposit). Sao independentes entre si, todos brand-gated e desligados por default no base, e juntos somam ~25 arquivos entre config, store, hooks, services, providers e componentes.
| Subsistema | Quando dispara | Gate |
|---|---|---|
| FTD Cashback (D0) | Depois do FTD, monitorando o saldo em jogos elegiveis | featuresConfig.ftdCashback.enabled + lista de jogos elegiveis nao vazia |
| Daily check-in (D1) | Depois do FTD, recompensa por dia consecutivo (Smartico) | ftdCheckinConfig.enabled |
| FTD Offer | Antes do FTD, quando o usuario fecha o modal de deposito sem depositar | featuresConfig.ftdOffer.enabled |
:::info Default no base = tudo desligado
app/config/ftd-checkin/ftd-checkin.ts envia enabled: false; app/config/cashback/eligible-games.ts envia uma lista vazia; e featuresConfig.ftdCashback / ftdOffer sao opcionais. Uma brand sem override nao carrega nada disso. Hoje as brands que ligam algum desses fluxos sao 7k-bet-br e cl-bet7k-com — consulte overrides/*/app/config/{ftd-checkin,cashback}/ e overrides/*/app/config/features/features.ts pra lista atual.
:::
Todos os fluxos leem auth de useAccountsStore (@cactus-agents/accounts/react), nunca de loaderData — ver State Management.
Kill-switch remoto compartilhado
Os tres subsistemas usam o mesmo padrao de kill-switch remoto, pra que marketing desligue uma campanha sem deploy:
featureFlags?: {
legacy?: string[]; // keys lidas do provider legado de feature-flag
};
Semantica: todas as keys listadas precisam resolver true pra que o gate passe. Lista ausente ou vazia = no-op (true).
A implementacao vive em:
| Arquivo | Papel |
|---|---|
app/services/legacy-feature-flag.client.ts | fetchLegacyFeatureFlag(key) — um GET por key, resposta booleana |
app/hooks/useLegacyFeatureFlags.ts | useLegacyFeatureFlags(keys) — agrega as keys num allTrue |
Detalhes que importam operacionalmente:
- O endpoint e hardcoded de proposito — e a infraestrutura legada que marketing ja opera. Brands que nao declaram nenhuma key nunca chamam o servico.
- Cache em escopo de modulo: a primeira chamada por key resolve, as concorrentes reusam a mesma Promise, e o valor vale pela sessao da pagina (sem refetch).
- Qualquer falha resolve
false: rede offline, ad-blocker, CORS, 5xx, resposta nao-JSON, oudata.valueque nao seja o booleanotrue. - Client-only. Chamar no server falha; callers devem gatear com
typeof window !== "undefined". - Otimizacao de auth: o
useFtdCheckinAccesspassa lista vazia enquanto o usuario e anonimo, porque o gate exigeisAuthenticatedde qualquer forma — evita RTT desperdicado em toda home. No login o memo re-avalia e os fetches disparam.
FTD Cashback (D0)
Fluxo de retencao pos-FTD que monitora o saldo do usuario dentro de jogos elegiveis e dispara comunicacoes quando o saldo cai abaixo de thresholds relativos ao valor do FTD.
Arquivos
| Arquivo | Papel |
|---|---|
app/config/cashback/eligible-games.ts | eligibleGameIds: ReadonlyArray<string> — IDs de jogos elegiveis |
app/hooks/useFtdCashbackFlow.ts | Maquina de estados do fluxo (gates, polling, modais) |
app/hooks/useFtdCashbackEligibility.ts | resolveFtdCashbackEligibility() — elegibilidade local |
app/components/ftd-cashback/FtdCashbackProvider.tsx | Monta o fluxo dentro da pagina de jogo |
app/components/ftd-cashback/FtdCashbackFirstModal.tsx | 1ª comunicacao (variant default) |
app/components/ftd-cashback/FtdCashbackPrizeModal.tsx | Modal animado do cashback |
app/components/ftd-cashback/ftd-cashback-tiers.ts | DEFAULT_FTD_CASHBACK_TIERS + resolveFtdCashbackBonus() |
app/components/ftd-cashback/ftd-cashback-storage.ts | Dedup por sessao (sessionStorage) |
app/components/ftd-cashback/ftd-cashback-analytics.ts | trackFtdCashbackEvent() |
app/components/ftd-cashback/ftd-cashback-dev.ts | Flags de dev (isDevMode, readDevFlags) |
app/services/ftd-cashback.client.ts | verifyEligibility() / sendCashback() |
app/routes/api/ftd-cashback/verify.ts | Proxy → Dark Verifier |
app/routes/api/ftd-cashback/send-cashback.ts | Proxy → Dark Freedom |
app/routes/api/ftd-cashback/_aws-endpoints.server.ts | URLs default das APIs AWS |
app/routes/dev.ftd-cashback.tsx | Painel de dev (so em dev) |
O FtdCashbackProvider e montado na pagina de jogo (app/routes/games/$provider.$game.tsx) recebendo o gameId.
Endpoints AWS e o proxy obrigatorio
As APIs externas do fluxo sao lambdas na AWS (sa-east-1), herdadas do legado 7k. Os defaults vivem em app/routes/api/ftd-cashback/_aws-endpoints.server.ts:
export const DEFAULT_DARK_VERIFIER_URL =
"https://li6oijg43l.execute-api.sa-east-1.amazonaws.com/dark-verifier";
export const DEFAULT_DARK_FREEDOM_URL =
"https://ff1vva6bk2.execute-api.sa-east-1.amazonaws.com/dark";
:::danger As chamadas AWS PRECISAM passar por proxy server-side
Toda a logica do fluxo e client-side, mas as chamadas as APIs Dark nao podem sair do browser: o payload carrega o encrypted_user_id, tratado como segredo. Os proxies resolvem esse ID do JWT via getAuthForRequest e montam o body — o cliente nunca ve o valor.
O prefixo _ em _aws-endpoints.server.ts garante que o React Router nao trate o arquivo como rota, e o suffix .server.ts impede inclusao no bundle do client.
:::
Contratos (paridade com o legado — atencao aos nomes dos campos):
| Proxy | Body enviado a AWS | Resposta normalizada pro UI |
|---|---|---|
POST /api/ftd-cashback/verify | { encrypted_id, brand_id } | { ok: true, eligible: boolean } |
POST /api/ftd-cashback/send-cashback | { id, bonus_value, brand_id, wallet_balance } | { ok, … } |
Repare que o verify usa encrypted_id e o send-cashback usa id pro mesmo valor. O client envia camelCase ({ walletBalance, bonusValue, stt? }) e o proxy converte.
O campo stt ("state") distingue os gates: 1 = cashback (gate 2), 2 = saldo bonus (gate 4). Callers antigos omitem e o proxy assume 1.
Ambos os proxies curto-circuitam com { ok: false, error: "feature_disabled" } quando featuresConfig.ftdCashback.enabled e falsy, e com 401 sem token.
O send-cashback e single-attempt por sessao, sem retry: se falhar, o usuario pode reabrir o jogo numa sessao nova.
Brands em outra regiao (ou testes contra staging) sobrescrevem via featuresConfig.ftdCashback.apis.{darkVerifier,darkFreedom}.
Config — FtdCashbackConfig
Campos principais (type completo em app/types/feature-flags.ts):
| Campo | Default | Descricao |
|---|---|---|
enabled | — | Master switch |
brandId | — (obrigatorio quando habilitado) | ID da brand no payload das APIs Dark |
depositDaysPeriod | 2 | Janela de elegibilidade em dias apos o FTD |
firstAlertBalanceRatio | 0.8 | Threshold da 1ª comunicacao (saldo = 80% do FTD) |
cashbackAlertBalanceRatio | 0.2 | Threshold do cashback final (saldo = 20% do FTD) |
walletPollingIntervalMs | 31_000 | Intervalo de polling do saldo |
bonusTiers | DEFAULT_FTD_CASHBACK_TIERS | Tabela FTD → cashback |
apis | endpoints default | Override dos endpoints AWS |
assets | fallback neutro | Ilustracoes dos modais |
modalVariant | "default" | "default" | "vera-legacy" |
copy / templateAssets | defaults PT-BR + CDN | Strings e assets da variant vera-legacy |
saldoBonus | ausente (off) | Gates 3 e 4 (STT 2) |
featureFlags.legacy | [] | Kill-switch remoto |
Tiers
DEFAULT_FTD_CASHBACK_TIERS mapeia o valor do FTD ao premio. A convencao das faixas e importante:
maxe o limite exato da faixa (8,15, …) e o tier seguinte comeca em+R$ 0,01(8.01) — faixas contiguas em centavos, sem overlap nem gap.- O resolver compara em cents (
Math.round(value * 100)), com match inclusivo nas duas pontas — evita aritmetica de float em valores como8.00/8.01. - O ultimo tier e catch-all (
max: Number.POSITIVE_INFINITY).
Brands sobrescrevem a tabela inteira via bonusTiers.
Os quatro gates
Os thresholds sao ratios do valor do FTD (saldo caindo):
| Gate | Ratio default | Comunicacao |
|---|---|---|
| 1 — primeiro aviso | 0.8 (saldo = 80%) | first-bonus |
| 2 — cashback | 0.2 (saldo = 20%) | cashback + sendCashback(stt: 1) |
| 3 — pre-STT 2 | 0.08 (saldo = 8%) | pre-saldo-bonus |
| 4 — saldo bonus (STT 2) | 0.05 (saldo = 5%) | saldo-bonus + sendCashback(stt: 2) |
Os gates 3 e 4 sao opt-in via saldoBonus.enabled e so fazem sentido com modalVariant: "vera-legacy" (os renderers deles sao o template unificado). Eles tem kill-switch remoto proprio (saldoBonus.featureFlags.legacy).
FtdCashbackActiveModal = "none" | "first-bonus" | "cashback" | "pre-saldo-bonus" | "saldo-bonus".
Dedup
ftd-cashback-storage.ts grava flags em sessionStorage (tab-scoped, sobrevive a reload) — uma por evento (ftd_cashback_first_shown, ftd_cashback_sent, ftd_cashback_prize_shown, ftd_cashback_session_burned, e os equivalentes de STT 2). O hook chama o Dark Verifier a cada abertura de jogo, entao o storage e o que impede repeticao. clearAll() limpa tudo no logout.
Variants visuais
modalVariant | 1ª comunicacao | 2ª comunicacao |
|---|---|---|
"default" | FtdCashbackFirstModal (modal central, strings via i18n payments.ftd_cashback.*) | FtdCashbackPrizeModal (modal central animado) |
"vera-legacy" | toast in-game (slide-in top-right, imagem + texto curto) | modal cashback do template unificado (hero + counter + CTA) |
Na variant vera-legacy as strings vem de copy e os assets de templateAssets; quando ausentes, caem nos defaults PT-BR + assets de CDN. As cores saem dos tokens de tema ftd-cashback-* / ftd-offer-*.
Daily check-in (D1)
Recompensa por dias consecutivos apos o FTD, dirigida por Smartico: cada missao de uma custom section vira o calendario, e cada task dentro da missao vira um dia.
Arquivos
| Arquivo | Papel |
|---|---|
app/config/ftd-checkin/ftd-checkin.ts | ftdCheckinConfig |
app/types/ftd-checkin.ts | FtdCheckinConfig (type documentado campo a campo) |
app/store/ftdCheckin.ts | useFtdCheckinStore — missoes, section id, claim |
app/hooks/useFtdCheckinAccess.ts | Gate de acesso + refresh |
app/utils/ftd-checkin.ts | Helpers de calendario e status de bonus |
app/services/smartico-checkin.client.ts | loadCheckinMissionsFromService() |
app/services/smartico-checkin.fixtures.ts | Fixture estatica pro modo mock |
app/components/ftd/checkin/Checkin.tsx | Painel do check-in |
app/components/ftd/checkin/CheckinTrigger.tsx | Trigger na home |
app/components/ftd/checkin/CheckinClaimModal.tsx | Modal de resgate |
app/components/ftd/checkin/CheckinStoreOffers.tsx | Bloco "Ofertas especiais" |
app/components/ftd-checkin-announcement/ | Announcement (modal ou thumb de stories) |
CheckinTrigger e montado pela home (app/routes/_index.tsx); FtdCheckinAnnouncementProvider pelo DefaultLayout.
Config
export const ftdCheckinConfig: FtdCheckinConfig = {
enabled: false,
sectionName: "CHECKIN_DIARIO",
allowMockQuery: false,
showOffers: true,
};
| Campo | Default | Descricao |
|---|---|---|
enabled | false | Master switch |
sectionName | "CHECKIN_DIARIO" | Nome da custom section do Smartico que agrupa as missoes |
allowMockQuery | false | Aceita ?mockMissions=true pra carregar fixture. Sempre gateado por import.meta.env.DEV alem disso |
showOffers | true | Secao "Ofertas especiais" (itens da loja Smartico) dentro do painel |
featureFlags.legacy | [] | Kill-switch remoto |
announcement | { enabled: false } | Modal/thumb de boas-vindas ao check-in |
Como a resolucao da section funciona: a store chama getCustomSections() pra achar o id da secao pelo nome e filtra as missoes por mission.custom_section_id === sectionId.
:::note showOffers e independente do VIP store — mas nao suficiente
showOffers nao e o mesmo flag de gamificationConfig.modules.store.enabled. A regra efetiva e a conjuncao: as ofertas so aparecem quando showOffers e truthy e o modulo global de store esta habilitado (as ofertas vem da mesma API do Smartico).
:::
Gate de acesso — useFtdCheckinAccess()
Todas as condicoes abaixo sao necessarias:
ftdCheckinConfig.enableduseAccountsStore.isAuthenticateduseGamificationStore.ready(SDK Smartico + dados carregados)user.ftd_value > 0- Todas as keys de
featureFlags.legacyresolvendotrue - Missoes retornadas pelo Smartico pra secao configurada
:::warning Use ftd_value, nao hasFirstDeposit
O gate usa user.ftd_value > 0 deliberadamente. useWalletFlags().hasFirstDeposit envolve wallet.real.hasWalletDeposit, que tem semantica diferente — qualquer transacao de carteira, inclusive bonus. O legado usa ftd_value pela mesma razao.
:::
Announcement
O card "Garanta sua diversao!" que convida ao check-in. Toggle independente do D0:
announcement?: {
enabled: boolean;
mode?: "modal" | "stories"; // default "modal"
frequency?: "once-per-day" | "once-per-session"; // default "once-per-day"
copy?: { title?: string; descriptionHtml?: string; ctaText?: string };
templateAssets?: { image?: string };
};
mode | Comportamento |
|---|---|
"modal" | Modal central. Frequencia controlada por frequency |
"stories" | Injeta um thumb no primeiro slot do carrossel <StoriesCircles> (mobile). Click abre o painel. Nao usa storage local — fica visivel enquanto o usuario nao fez o check-in do dia |
Sao mutuamente exclusivos. mode: "stories" exige que a brand tenha <StoriesCircles> montado na home, senao o thumb nao tem onde aparecer.
Frequencia (so no modo modal):
"once-per-day"— grava a dataYYYY-MM-DDlocal emlocalStorage(ftd_checkin_announcement_last_shown_date). Sobrevive a logout/login no mesmo dia, fechar/reabrir aba e multiplas abas; reseta ao virar o dia."once-per-session"— grava flag emsessionStorage(ftd_checkin_announcement_shown); reaparece a cada nova aba.
Trocar de modo a quente e seguro: o storage do modo anterior fica inerte (a chave permanece, e ignorada).
O gate do announcement continua dependendo de ftdCheckinConfig.enabled + canAccess. Se o D1 esta off, o announcement tambem esta.
FTD Offer
Oferta relampago pra usuario que ainda nao depositou.
Arquivos
| Arquivo | Papel |
|---|---|
app/hooks/useFtdOfferFlow.ts | Maquina de estados (elegibilidade, modal, widget) |
app/hooks/useFtdOfferCountdown.ts | Countdown formatado |
app/components/ftd-offer/FtdOfferProvider.tsx | Provider montado no DefaultLayout |
app/components/ftd-offer/FtdOfferModal.tsx | Modal central (variant default) |
app/components/ftd-offer/FtdOfferFloatingWidget.tsx | Widget flutuante (variant default) |
app/components/ftd-offer/FtdOfferStoryThumb.tsx | Thumb no slot de stories |
app/components/ftd-offer/ftd-offer-storage.ts | ftd_offer_shown + ftd_offer_ends_at (sessionStorage) |
app/components/ftd-offer/ftd-offer-analytics.ts | trackFtdOfferEvent() |
app/components/ftd-offer-modal/ | Template unificado da variant vera-legacy |
Trigger e ciclo
- Gate:
featuresConfig.ftdOffer.enabled+ usuario autenticado + sem FTD + auth hidratada. - Trigger default: o usuario abre e fecha o modal/drawer de deposito sem depositar.
- Sobe o modal central com countdown.
- Ao fechar o modal sem depositar, um widget compacto com o countdown fica visivel (slot de stories ou flutuante) e permite reabrir o modal.
- O CTA do modal fecha a oferta e reabre o drawer de deposito.
- Quando o usuario faz o FTD, o widget e desmontado apos
postDepositGraceSeconds.
Tudo dura uma sessao de browser (sessionStorage: ftd_offer_shown, ftd_offer_ends_at).
Config — FtdOfferConfig
| Campo | Default | Descricao |
|---|---|---|
enabled | — | Master switch |
countdownSeconds | 300 | Duracao do countdown |
postDepositGraceSeconds | 300 | Tempo que o widget segue visivel apos o FTD |
widgetMode | "auto" | "auto" | "stories-first" | "floating" | "off" |
modalVariant | "default" | "default" | "vera-legacy" |
openOnLogin | false | Abre o modal ja no login, sem esperar o trigger do deposito |
assets | fallback | modalIllustration, storyThumb, floatingThumb |
openOnLogin: true combina com widgetMode: "stories-first": o modal abre na entrada, o usuario fecha, e o thumb fica no carrossel pra reabrir.
As duas variants sao funcionalmente identicas — muda layout, ilustracoes e gradientes. "vera-legacy" renderiza FtdOfferModalContainer variant="flash-offer" + FtdOfferFloatingTrigger.
Providers no layout
DefaultLayout monta a cadeia (ver Layout):
… → FtdOfferProvider → FtdCheckinAnnouncementProvider → (children)
FtdCashbackProvider nao entra aqui — ele e montado dentro da pagina de jogo, com o gameId.
Tokens de tema
app/config/theme/colors.ts traz dois grupos dedicados, brand-overridable:
| Grupo | Consumido por |
|---|---|
ftd-cashback | app/components/ftd-cashback/* — first-bg-from/to, first-text-accent, prize-bg-from/to, prize-amount-color, prize-glow-color, prize-coin-color |
ftd-offer | app/components/ftd-offer/* e o template unificado — gradientes do modal e da ilustracao, text-accent, bloco de countdown, borda e badge do thumb |
Os defaults reaproveitam paletas existentes pra nao destoar quando uma brand liga a flag sem customizar o tema. Ver Theming.
:::warning Override e substituicao de arquivo
Ao adicionar um campo em qualquer uma dessas configs (features.ts, ftd-checkin.ts, theme/colors.ts, cashback/eligible-games.ts), propague pra todos os overrides que ja tem o arquivo — senao aquela brand recebe undefined. Ver Override Files.
:::