## CC-5 — Entrar no bolão pelo link

**PRD:** `docs/prd/criacao-e-convite.md`

**Como** participante casual, **quero** abrir o link do grupo e cair no bolão
certo em poucos toques, **para** não deixar para depois — porque "depois" é
depois do prazo.

**Prioridade:** Must — cada passo entre o link e o primeiro palpite é gente
que não entra.

**Critérios de aceite:**

- Dado que recebi o link e não tenho o app instalado, quando abro o link,
  então depois de instalar eu caio **no bolão daquele convite**, não numa tela
  genérica.
- Dado que tenho o app e estou identificado, quando abro o link, então entro
  no bolão em poucos toques e vejo as regras e o próximo prazo.
- Dado que já sou participante daquele bolão, quando abro o link de novo,
  então vou para o bolão sem entrar duas vezes nem duplicar meu cadastro.
- Dado que o link foi invalidado ou o bolão encerrou, quando abro, então vejo
  mensagem explicando, não uma tela de erro genérica.
- Dado que o primeiro jogo do bolão já começou, quando abro o convite, então a
  entrada é **recusada** com mensagem explicando que o bolão já começou — não
  é possível entrar atrasado.
- Dado que eu já participei deste bolão e saí, quando abro o convite, então a
  entrada é recusada — a saída é definitiva (CV-4).

**Tasks:**

_Backend_
- [x] `BE-1` Endpoint de entrada no bolão a partir do token de convite (depende de CC-4 BE-1)
- [x] `BE-2` Idempotência: reentrada de quem já é participante não duplica vínculo (depende de BE-1)
- [x] `BE-3` Recusar entrada com convite inválido ou bolão encerrado, com erro distinguível (depende de BE-1)
- [x] `BE-4` **Recusar entrada depois que o primeiro jogo do bolão começou** — não é possível entrar atrasado (depende de BE-1, CC-1 BE-3)
- [x] `BE-5` Recusar entrada de quem já participou e saiu, pela mesma conta (depende de BE-1, CV-4 BE-5)

_Mobile_
- [x] `FE-1` Deep link que resolve o bolão certo, inclusive após instalar o app (cold start)
- [x] `FE-2` Fluxo de entrada mínimo, terminando na tela de palpite do bolão (depende de BE-1)
- [x] `FE-3` Telas de recusa, com mensagem distinta para cada motivo: convite inválido, bolão encerrado, bolão já começou, já saiu do bolão (depende de BE-3, BE-4, BE-5)
- [x] `FE-4` Exibir regras e próximo prazo logo após entrar, no fuso local

_QA_
- [x] `QA-1` Escrever o plano de casos de teste a partir desta story
- [x] `QA-2` Executar o plano com app não instalado, app fechado, reentrada, convite inválido e campeonato em andamento

**Riscos/Dependências:**

- **Deep link com app não instalado é o caso que mais quebra na prática** — e
  é exatamente o caminho do convidado novo.
- Depende de CC-4 e de AC-1 (cadastro), que é quem identifica a pessoa que
  abriu o convite.
- **Decidido em 03/08: não é possível entrar atrasado** — a entrada fecha no
  início do primeiro jogo (`BE-4`). Isso eliminou a regra de "entrada no meio"
  e **cancelou a story PR-9** (pontos iniciais), que não existe mais.
- **A janela de entrada é estreita:** o convite expira em 1 dia (CC-4) e a
  entrada fecha no primeiro jogo. Quem for reconvidado numa temporada nova
  (CV-7) tem os dois limites contra si.

- **06/08:** `FE-1` (deep link) segue aberta. A entrada em produção hoje é por
  **código colado** na tela inicial, que é o caminho verificado em device. Sem o
  deep link, o texto compartilhado precisa dizer o que fazer com o código — e
  diz (`CC-4 FE-1`).

- **06/08:** plano de casos em `docs/testes/CC-5-plano.md`. Escrito a partir da
  story, não do código — caso escrito depois de olhar a implementação valida o
  que foi feito, não o que foi pedido.

- **06/08 — execução: ⚠️ parcial, 10 de 12** (`docs/testes/CC-5-execucao.md`).
  `QA-2` **fica aberta**: `CC-5-02` depende de `AC-1 FE-4` e `CC-5-12` de
  `CC-5 FE-4`, as duas abertas. Marcar com ressalva faria o time ler a story como
  testada por inteiro.
- `CC-5-11` achou um bug alto: o diálogo de entrada por convite descartava o
  `TextEditingController` na linha seguinte ao `await showDialog`, e
  `showDialog` retorna quando a rota **começa** a sair — o `TextField` continua
  sendo construído durante a animação de fechamento. Resultado: asserção do
  framework ("used after being disposed") e `Overlay` inconsistente, no funil de
  entrada de todo participante novo. Corrigido extraindo o diálogo para um widget
  que **é dono** do controller.
- Detalhe que vale registrar: eu já tinha aberto e usado esse diálogo em
  verificação manual no emulador e **não percebi** — a exceção não interrompia o
  fluxo o suficiente. Foi o teste de widget que a tornou visível.

- **06/08 — deep link feito e verificado em device**, com o app **fechado**:
  `bolao://bolao.app/convite/<token>` abre o app, entra no bolão e cai na tela de
  palpite com as regras e o próximo prazo. Execução do plano agora **✅ 12 de 12**.
- `FE-2` estava marcada e **estava incompleta**: o app mostrava aviso e recarregava
  a lista, deixando o participante procurar e tocar no bolão. Um passo extra
  exatamente onde a story pede o mínimo. Agora vai direto para a tela de palpite.
- Três defeitos apareceram no caminho, todos **silenciosos** — o `intent-filter`
  funcionava e o link não fazia nada:
  1. **Formato do link.** Com esquema próprio o Flutter recebe só o `path` da URI,
     então `bolao://convite/<token>` chegava como `/<token>`. Host fixo
     (`bolao.app`) e o "convite" no caminho resolveram.
  2. **`flutter_deeplinking_enabled` ausente.** Sem essa meta-data o embedding
     Android abre o app pelo intent mas **não entrega a URI ao Router**.
  3. **Riverpod 3 descarta todo provider por padrão.** O router grava o token
     dentro do `redirect`, quando ninguém observa aquele provider — sem
     `ref.keepAlive()` o token morria antes de a tela inicial existir.
  4. **Ordem do `redirect`.** Eu havia posto a checagem do convite **depois** da
     guarda de "sessão carregando", e no cold start a URI chega sempre durante a
     restauração da sessão — que é justamente o caso do link, porque foi ele que
     abriu o app. A guarda engolia o token.
- ⚠️ Consequência aceita do esquema próprio: o link **só abre o app se o app
  estiver instalado**. App Link `https` exigiria domínio verificado com
  `assetlinks.json` hospedado, e não há domínio nesta versão. Por isso o texto
  compartilhado leva o link **e** o código, para quem ainda não instalou.
