Pular para o conteúdo principal

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.

SubsistemaQuando disparaGate
FTD Cashback (D0)Depois do FTD, monitorando o saldo em jogos elegiveisfeaturesConfig.ftdCashback.enabled + lista de jogos elegiveis nao vazia
Daily check-in (D1)Depois do FTD, recompensa por dia consecutivo (Smartico)ftdCheckinConfig.enabled
FTD OfferAntes do FTD, quando o usuario fecha o modal de deposito sem depositarfeaturesConfig.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:

ArquivoPapel
app/services/legacy-feature-flag.client.tsfetchLegacyFeatureFlag(key) — um GET por key, resposta booleana
app/hooks/useLegacyFeatureFlags.tsuseLegacyFeatureFlags(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, ou data.value que nao seja o booleano true.
  • Client-only. Chamar no server falha; callers devem gatear com typeof window !== "undefined".
  • Otimizacao de auth: o useFtdCheckinAccess passa lista vazia enquanto o usuario e anonimo, porque o gate exige isAuthenticated de 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

ArquivoPapel
app/config/cashback/eligible-games.tseligibleGameIds: ReadonlyArray<string> — IDs de jogos elegiveis
app/hooks/useFtdCashbackFlow.tsMaquina de estados do fluxo (gates, polling, modais)
app/hooks/useFtdCashbackEligibility.tsresolveFtdCashbackEligibility() — elegibilidade local
app/components/ftd-cashback/FtdCashbackProvider.tsxMonta o fluxo dentro da pagina de jogo
app/components/ftd-cashback/FtdCashbackFirstModal.tsx1ª comunicacao (variant default)
app/components/ftd-cashback/FtdCashbackPrizeModal.tsxModal animado do cashback
app/components/ftd-cashback/ftd-cashback-tiers.tsDEFAULT_FTD_CASHBACK_TIERS + resolveFtdCashbackBonus()
app/components/ftd-cashback/ftd-cashback-storage.tsDedup por sessao (sessionStorage)
app/components/ftd-cashback/ftd-cashback-analytics.tstrackFtdCashbackEvent()
app/components/ftd-cashback/ftd-cashback-dev.tsFlags de dev (isDevMode, readDevFlags)
app/services/ftd-cashback.client.tsverifyEligibility() / sendCashback()
app/routes/api/ftd-cashback/verify.tsProxy → Dark Verifier
app/routes/api/ftd-cashback/send-cashback.tsProxy → Dark Freedom
app/routes/api/ftd-cashback/_aws-endpoints.server.tsURLs default das APIs AWS
app/routes/dev.ftd-cashback.tsxPainel 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):

ProxyBody enviado a AWSResposta 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):

CampoDefaultDescricao
enabledMaster switch
brandId— (obrigatorio quando habilitado)ID da brand no payload das APIs Dark
depositDaysPeriod2Janela de elegibilidade em dias apos o FTD
firstAlertBalanceRatio0.8Threshold da 1ª comunicacao (saldo = 80% do FTD)
cashbackAlertBalanceRatio0.2Threshold do cashback final (saldo = 20% do FTD)
walletPollingIntervalMs31_000Intervalo de polling do saldo
bonusTiersDEFAULT_FTD_CASHBACK_TIERSTabela FTD → cashback
apisendpoints defaultOverride dos endpoints AWS
assetsfallback neutroIlustracoes dos modais
modalVariant"default""default" | "vera-legacy"
copy / templateAssetsdefaults PT-BR + CDNStrings e assets da variant vera-legacy
saldoBonusausente (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:

  • max e 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 como 8.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):

GateRatio defaultComunicacao
1 — primeiro aviso0.8 (saldo = 80%)first-bonus
2 — cashback0.2 (saldo = 20%)cashback + sendCashback(stt: 1)
3 — pre-STT 20.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

modalVariant1ª comunicacao2ª 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

ArquivoPapel
app/config/ftd-checkin/ftd-checkin.tsftdCheckinConfig
app/types/ftd-checkin.tsFtdCheckinConfig (type documentado campo a campo)
app/store/ftdCheckin.tsuseFtdCheckinStore — missoes, section id, claim
app/hooks/useFtdCheckinAccess.tsGate de acesso + refresh
app/utils/ftd-checkin.tsHelpers de calendario e status de bonus
app/services/smartico-checkin.client.tsloadCheckinMissionsFromService()
app/services/smartico-checkin.fixtures.tsFixture estatica pro modo mock
app/components/ftd/checkin/Checkin.tsxPainel do check-in
app/components/ftd/checkin/CheckinTrigger.tsxTrigger na home
app/components/ftd/checkin/CheckinClaimModal.tsxModal de resgate
app/components/ftd/checkin/CheckinStoreOffers.tsxBloco "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,
};
CampoDefaultDescricao
enabledfalseMaster switch
sectionName"CHECKIN_DIARIO"Nome da custom section do Smartico que agrupa as missoes
allowMockQueryfalseAceita ?mockMissions=true pra carregar fixture. Sempre gateado por import.meta.env.DEV alem disso
showOfferstrueSecao "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:

  1. ftdCheckinConfig.enabled
  2. useAccountsStore.isAuthenticated
  3. useGamificationStore.ready (SDK Smartico + dados carregados)
  4. user.ftd_value > 0
  5. Todas as keys de featureFlags.legacy resolvendo true
  6. 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 };
};
modeComportamento
"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 data YYYY-MM-DD local em localStorage (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 em sessionStorage (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

ArquivoPapel
app/hooks/useFtdOfferFlow.tsMaquina de estados (elegibilidade, modal, widget)
app/hooks/useFtdOfferCountdown.tsCountdown formatado
app/components/ftd-offer/FtdOfferProvider.tsxProvider montado no DefaultLayout
app/components/ftd-offer/FtdOfferModal.tsxModal central (variant default)
app/components/ftd-offer/FtdOfferFloatingWidget.tsxWidget flutuante (variant default)
app/components/ftd-offer/FtdOfferStoryThumb.tsxThumb no slot de stories
app/components/ftd-offer/ftd-offer-storage.tsftd_offer_shown + ftd_offer_ends_at (sessionStorage)
app/components/ftd-offer/ftd-offer-analytics.tstrackFtdOfferEvent()
app/components/ftd-offer-modal/Template unificado da variant vera-legacy

Trigger e ciclo

  1. Gate: featuresConfig.ftdOffer.enabled + usuario autenticado + sem FTD + auth hidratada.
  2. Trigger default: o usuario abre e fecha o modal/drawer de deposito sem depositar.
  3. Sobe o modal central com countdown.
  4. Ao fechar o modal sem depositar, um widget compacto com o countdown fica visivel (slot de stories ou flutuante) e permite reabrir o modal.
  5. O CTA do modal fecha a oferta e reabre o drawer de deposito.
  6. 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

CampoDefaultDescricao
enabledMaster switch
countdownSeconds300Duracao do countdown
postDepositGraceSeconds300Tempo que o widget segue visivel apos o FTD
widgetMode"auto""auto" | "stories-first" | "floating" | "off"
modalVariant"default""default" | "vera-legacy"
openOnLoginfalseAbre o modal ja no login, sem esperar o trigger do deposito
assetsfallbackmodalIllustration, 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:

GrupoConsumido por
ftd-cashbackapp/components/ftd-cashback/*first-bg-from/to, first-text-accent, prize-bg-from/to, prize-amount-color, prize-glow-color, prize-coin-color
ftd-offerapp/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. :::