From f5140b15a2254fb2ddcb5ee693f309c17f822cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sun, 19 Jul 2026 19:24:12 -0300 Subject: [PATCH] Polish campaign login auth UI after Impeccable critique. Fix header alignment and rhythm, swap Megaphone for CampaignLogo, add progressive disclosure for first access, and extract shared auth shells with corrected individual-account copy; record closed critique items in RS+. --- ...47-04Z__src-app-campaign-campanha-login.md | 91 +++++++++++++++++++ .../escala-dry-pos-reset-senha-perfil.md | 23 +++-- docs/roadmap.md | 66 +++++++------- .../esqueci-senha/ForgotPasswordForm.tsx | 37 +++----- .../(campaign)/campanha/login/LoginForm.tsx | 39 +++++--- .../redefinir-senha/ResetPasswordForm.tsx | 33 +++---- .../campanha/redefinir-senha/page.tsx | 12 +-- .../campaign/CampaignAuthBackToLoginLink.tsx | 11 +++ .../campaign/CampaignAuthCardHeader.tsx | 20 ++++ .../campaign/CampaignAuthPageShell.tsx | 9 +- src/lib/campaignAuthCopy.ts | 22 ++++- 11 files changed, 252 insertions(+), 111 deletions(-) create mode 100644 .impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md create mode 100644 src/components/campaign/CampaignAuthBackToLoginLink.tsx create mode 100644 src/components/campaign/CampaignAuthCardHeader.tsx diff --git a/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md new file mode 100644 index 00000000..a9ecdf6e --- /dev/null +++ b/.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md @@ -0,0 +1,91 @@ +--- +target: /campanha/login +total_score: 26 +p0_count: 0 +p1_count: 2 +timestamp: 2026-07-19T21-47-04Z +slug: src-app-campaign-campanha-login +--- +# Critique: /campanha/login + +Method: dual-agent (A: 8a8b1ae0-9a8a-45f7-907f-54afcdf62db8 · B: 3899acad-4f36-4817-91d6-4135d72d2e96) + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 3 | Pending state clear; no other status chrome | +| 2 | Match System / Real World | 3 | PT-BR + e-mail/celular fit; megaphone mark generic | +| 3 | User Control and Freedom | 2 | Only “Esqueceu a senha?”; no exit to public site | +| 4 | Consistency and Standards | 2 | h1 left vs centered muted copy; leading clash | +| 5 | Error Prevention | 2 | required + inputMode help; dual identifier still easy to mistype | +| 6 | Recognition Rather Than Recall | 3 | Labels/placeholder clear | +| 7 | Flexibility and Efficiency | 3 | E-mail or phone + adaptive inputMode | +| 8 | Aesthetic and Minimalist Design | 2 | Header rhythm/alignment + always-on help prose | +| 9 | Error Recovery | 3 | FieldError + leadership recovery hint | +| 10 | Help and Documentation | 3 | Help exists; always-visible first-access competes | +| **Total** | | **26/40** | **Acceptable** | + +## Anti-Patterns Verdict + +**LLM:** Low–moderate slop. Not purple-SaaS; stock shadcn auth shell. Dual muted blurbs feel like copy padding. Typesetting bugs read unfinished. + +**Detector CLI:** exit 0, 0 source findings on login + CampaignAuthPageShell. + +**Runtime (Playwright inject):** `flat-type-hierarchy` (body; sizes 14/16/20px); `nested-cards` on CardHeader — likely false positive (shadcn Card > CardHeader). + +## Overall Impression + +Competent Field Desk palette discipline, phone-first form craft — undermined by a campaign-theme `h1 { text-align: left }` leak and article-style `p { leading-7 }` on auth copy. Biggest opportunity: fix header typesetting; do **not** add split-screen public posts. + +## What's Working + +1. Phone-first: min-h-11, enterKeyHint, identifierInputMode +2. Red as signal ≤10% +3. Auth-error recovery hint for liderança phone-only accounts + +## Priority Issues + +### P1 — Title misaligned with subtitle +Campaign `[data-theme='campaign'] h1 { text-align: left }` overrides CardHeader `text-center`. Title glyph start ~28px left of centered subtitle. +**Fix:** Scope left-align to app chrome, or `text-center` on auth heading. +**Command:** `/impeccable typeset` or `/impeccable layout` + +### P1 — Subtitle vertical rhythm inverted +CardHeader `gap-1` (4px) between title and description; orphan `

` inherits base `leading-7` (28px) while CardDescription stays ~20px. Title glued to howto; second muted block breathes like article body. +**Fix:** One muted block with explicit leading-snug; bump title→desc gap; never bare `

` under campaign theme without leading override. +**Command:** `/impeccable layout` + `/impeccable typeset` + +### P2 — Split-screen + public publications: recommend No +Login is ops gate (noindex); posts are public register. Phone users get scroll soup. Motivation ≠ news feed on unlock. +**Soft maybe:** one quiet purpose line under mark — not a publication panel. +**Command:** `/impeccable shape` only if product insists on panel (internal voice, not posts) + +### P2 — Generic Megaphone mark +Not Solla / Field Desk identity. +**Command:** `/impeccable polish` (reuse campaign-logo) + +### P3 — Always-on first-access paragraph +Progressive disclosure for returning users. +**Command:** `/impeccable distill` or `/impeccable clarify` + +## Persona Red Flags + +**Casey:** Misalignment reads broken; two gray blurbs delay thumb-to-input; split-screen would be fatal. +**Jordan:** Copy helps; visual hierarchy undercuts trust; no public-site escape. +**Lia:** Input path strong; header bugs tax one-handed use; keep error recovery; reject post panel. +**Sam:** Muted contrast scrapes AA (~4.83:1); labels/ARIA look solid. + +## Minor Observations + +- CardHeader grid-rows assumes 2 rows; third orphan `

` is API misuse +- Desktop narrow column is correct for this task +- Placeholder dual-format is heavy + +## Questions to Consider + +1. If the desk is the product, why does the door still look like every shadcn login? +2. Is “inspiring” one quiet line under the mark — or only after auth? +3. Would merging the two muted sentences fix more trust than any split-screen? +4. Who unlocks at 7am in a núcleo — why would they want a publication? +5. Why does auth inherit dashboard `h1` left-align rules? diff --git a/docs/plans/escala-dry-pos-reset-senha-perfil.md b/docs/plans/escala-dry-pos-reset-senha-perfil.md index 439d77be..c796c690 100644 --- a/docs/plans/escala-dry-pos-reset-senha-perfil.md +++ b/docs/plans/escala-dry-pos-reset-senha-perfil.md @@ -1,7 +1,7 @@ # Escala e DRY pós-reset de senha + foto de perfil Status: registrado no roadmap (fases pendentes) -Atualizado em: 2026-07-19 (Fase 3 parcial entregue em `main` @ `246ca85`; absorção `capture-review-debts`) +Atualizado em: 2026-07-19 (Fase 3 parcial: polish `/campanha/login` + simplify; absorção `capture-review-debts` 2026-07-19) Item do roadmap: [docs/roadmap.md](../roadmap.md) (RS+, fill-in de engenharia pós-MVP) Responsável: — @@ -11,13 +11,15 @@ O fill-in **Reset de senha self-service + foto de perfil** ([reset-senha-foto-pe **Já resolvido no simplify (não reabrir):** `CampaignFormActionState` compartilhado; constantes de copy (`CAMPAIGN_PASSWORD_RESET_*`, `CAMPAIGN_LEADERSHIP_FORGOT_PASSWORD_MESSAGE`, `CAMPAIGN_SESSION_EXPIRED_MESSAGE`); `campaignUserShellView` / `CampaignUserShellView`; `canUpdateCampaignUserAvatar` delegando para `canUpdateCampaignUser`; `fieldError` nos forms; `FormActionStatus` no perfil; remoção de wrappers `passwordFormError`/`profileFormError`; exports internos de `resetCampaignPassword`/`changeCampaignPassword`; teste int renomeado para login com senha errada. -**Já resolvido na Fase 3 parcial (merge `main` 2026-07-19, `246ca85` — não reabrir):** `CampaignAuthPageShell` em `login` / `esqueci-senha` / `redefinir-senha` (incl. card de token inválido); `campaignAuthHeadingClassName` + copy compartilhada (`CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT`, `CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT`); login com `inputMode` dinâmico tel/email, `aria-invalid`/`aria-describedby` nos dois campos, copy de primeiro acesso, `w-full` no submit — fecha P1–P3 do critique Impeccable `/campanha/login` da sessão. +**Já resolvido na Fase 3 parcial (merge `main` 2026-07-19, `246ca85` — não reabrir):** `CampaignAuthPageShell` em `login` / `esqueci-senha` / `redefinir-senha` (incl. card de token inválido); `campaignAuthHeadingClassName` + copy compartilhada (`CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT`, `CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT`); login com `inputMode` dinâmico tel/email, `aria-invalid`/`aria-describedby` nos dois campos, `w-full` no submit. + +**Já resolvido no polish + `/simplify` de `/campanha/login` (2026-07-19 — não reabrir):** critique Impeccable P1–P3 fechados (alinhamento título/subtítulo `!text-center`, ritmo `gap-2` + `leading-snug`, `CampaignLogo` no shell, `

` para primeiro acesso); `CampaignAuthCardHeader`, `CampaignAuthBackToLoginLink`; tokens `campaignAuthDescriptionClassName`, `campaignAuthMutedTextClassName`, `campaignAuthTextLinkClassName`; `CAMPAIGN_LOGIN_SUBTITLE` (conta individual); `fieldError` sem dupla chamada em forgot/reset. Snapshot: [`.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md`](../../.impeccable/critique/2026-07-19T21-47-04Z__src-app-campaign-campanha-login.md). ## Objetivos - Auth hot path sem join de `media` em rotas que não exibem avatar (núcleos, apoiadores, planos, etc.). - Upload de avatar sem segurar transação Postgres aberta durante I/O de blob/sharp. -- Shell compartilhado das páginas públicas de auth (`login`, `esqueci-senha`, `redefinir-senha`) — **parcial ✓** (`CampaignAuthPageShell` + headings/copy; ver Fase 3). +- Shell compartilhado das páginas públicas de auth (`login`, `esqueci-senha`, `redefinir-senha`) — **parcial ✓** (shell + card header + logo + critique login fechados; pendem password fields + login state — Fase 3). - Um único módulo de validação/campos de senha reutilizado por convite, reset e perfil. - Login alinhado ao shape `CampaignFormActionState` + `mapCampaignFormActionError` das demais actions de campanha. - Guardrails: sem novo `Consent`; sem SMS/WhatsApp para token; comportamento de produto 1C inalterado salvo bugs encontrados. @@ -44,7 +46,7 @@ flowchart TD RS["Reset senha + perfil MVP ✓"] --> F1 F1["Fase 1 — Auth read perf
depth 0 + getCampaignUserWithAvatar
sessão leve em rotas públicas"] F1 --> F2["Fase 2 — Avatar write perf
blob fora da TX + revalidate estreito"] - F2 --> F3["Fase 3 — DRY auth UI/senha
shell ✓ parcial · password fields + login state"] + F2 --> F3["Fase 3 — DRY auth UI/senha
shell/card ✓ parcial · password fields + login state"] F3 --> F4["Fase 4 — DRY copy/role + actions
campaignRoleOptions + getCampaignActionContext"] F4 --> F5["Fase 5 — Opcional
avatar intent único + int change password"] ``` @@ -64,7 +66,7 @@ flowchart TD ### Fase 3 — DRY UI e senha -**Entregue parcialmente (2026-07-19):** `CampaignAuthPageShell` + headings/copy unificados nas três rotas públicas e no card de token inválido; polish de login do critique Impeccable (ver Contexto). +**Entregue parcialmente (2026-07-19):** `CampaignAuthPageShell`, `CampaignAuthCardHeader`, `CampaignAuthBackToLoginLink`, tokens de copy/classes em `campaignAuthCopy.ts`, `CampaignLogo` no shell, polish Impeccable `/campanha/login` (ver Contexto). **Pendente nesta fase:** @@ -74,6 +76,7 @@ flowchart TD ### Fase 4 — DRY copy, role e action context +- Consolidar strings de liderança/convite (`CAMPAIGN_LEADERSHIP_*`, `CAMPAIGN_FIRST_ACCESS_HINT`, `CAMPAIGN_LOGIN_SUBTITLE` + mensagem server em `campaignPasswordReset.ts`) numa fonte única com variantes por contexto. - **`campaignRoleLabels`** ↔ opções do campo `role` em `CampaignUser.ts` (fonte única `campaignRoleOptions`). - Propagar para **`NucleusUpdateFeed.tsx`** e outros `roleLabels` locais quando esses arquivos forem tocados. - **`password.ts` / `profile.ts`:** adotar `getCampaignActionContext()` (`src/utilities/campaignActionContext.ts`) como `supporter.ts` / `actionPlan.ts`. @@ -98,13 +101,21 @@ flowchart TD - RBAC em `users` (admin Payload) → [roadmap](../roadmap.md) Admin Payload. - Unificar **todo** o ciclo 1 de auth (convite + login + admin) num único mega-refactor — RS+ cobre só o fill-in entregue e vizinhos diretos. +## Explicitamente fora (critique/simplify desta sessão — não reabrir) + +- Split-screen login com publicações ou painel motivacional (decisão produto 2026-07-19; critique recomendou não). +- Link de saída para o site público na tela de login (heurística 3, score 2). +- Placeholder longo dual-format no identificador; submit helper global (`Spinner` + `aria-live`); constantes de rota auth; `flat-type-hierarchy` do detector runtime no body. +- Comentário em `styles.css` para auth — workaround com tokens/componentes já suficiente. + ## Referências - [reset-senha-foto-perfil.md](reset-senha-foto-perfil.md) — MVP entregue - [escala-dry-pos-visitados-recentemente.md](escala-dry-pos-visitados-recentemente.md) — precedente RS+/VR+ - `src/utilities/campaignAuth.ts`, `src/app/(campaign)/campanha/actions/password.ts`, `profile.ts` - `src/components/campaign/CampaignProfileSettings.tsx`, `CampaignSidebar.tsx`, `CampaignUserAvatar.tsx` -- `src/components/campaign/CampaignAuthPageShell.tsx` — shell entregue; ver Fase 3 pendente +- `src/components/campaign/CampaignAuthPageShell.tsx`, `CampaignAuthCardHeader.tsx`, `CampaignAuthBackToLoginLink.tsx` +- `src/lib/campaignAuthCopy.ts` - `src/app/(campaign)/campanha/login/`, `esqueci-senha/`, `redefinir-senha/` - `src/lib/schemas/campaignPassword.ts`, `src/lib/schemas/invite.ts` - `src/collections/CampaignUser.ts` diff --git a/docs/roadmap.md b/docs/roadmap.md index 74ce8402..24356a31 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -110,7 +110,7 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig | A3/A4 Baseline TSE 2022 + Gap vs 2022 | `Baseline-Eleitoral-2022` (+ bloco "Baseline 2022" em `Lista-Nucleos-Overview`) | [baseline-eleitoral-tse.md](plans/baseline-eleitoral-tse.md) | | A5 Insights (5 planos) | `Baseline-Eleitoral-2022` (card "Insights do território", uma linha por insight) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) (Fase 1 conversão da base + Fase 2 virada: vencedor dir. **ou** share dir. alto) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | | A7 Escala e DRY pós-A4 | — (sem design novo; reusa baseline A4 + Alert/Progress) | [escala-dry-pos-a4.md](plans/escala-dry-pos-a4.md) | -| B6 Escala e DRY pós-B3 | — (sem design novo; reusa mapa B3) | [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md) | +| B6 Escala e DRY pós-B3 | — (sem design novo; reusa mapa B3) | [escala-dry-pos-b3.md](plans/escala-dry-pos-b3.md) | | A8 Perfis do eleitorado (IBGE + manuais) | — (sem design nesta leva; reusa aba Eleitorado + `NucleusIntelligenceDialog`) | [perfil-eleitorado-ibge.md](plans/perfil-eleitorado-ibge.md) | | C2 Cadastro nominal de apoiadores | `Apoiadores-Lista`, `Apoiador-Ficha`, `Importar-CSV-Apoiadores` | [cadastro-nominal-apoiadores.md](plans/cadastro-nominal-apoiadores.md) | | C6 Escala e DRY pós-C2 | — (sem design novo; reusa os de C2 + padrões de núcleos) | [escala-dry-pos-c2.md](plans/escala-dry-pos-c2.md) | @@ -122,12 +122,12 @@ Os designs gerados pelo UX Pilot para os próximos ciclos estão em [`docs/desig | C11 Escala e DRY pós-C7 | — (sem design novo; reusa os de C3 + padrões de núcleos/C6/C9) | [escala-dry-pos-c7.md](plans/escala-dry-pos-c7.md) | | O0+ Escala e DRY pós-Onda 0 | — (sem design novo; reusa `/privacidade` + padrão revalidate `posts`) | [escala-dry-pos-onda0.md](plans/escala-dry-pos-onda0.md) | | VR+ Escala e DRY pós-visitados | — (sem design novo; reusa dashboard + padrões de lista de núcleos) | [escala-dry-pos-visitados-recentemente.md](plans/escala-dry-pos-visitados-recentemente.md) | -| RS+ Escala e DRY pós-reset senha/perfil | — (sem design novo; reusa login + perfil + padrões de form de campanha) | [escala-dry-pos-reset-senha-perfil.md](plans/escala-dry-pos-reset-senha-perfil.md) | -| FD+ Escala e DRY pós-Field Desk | — (sem design novo; reusa shells/strips Field Desk) | [escala-dry-pos-field-desk.md](plans/escala-dry-pos-field-desk.md) | -| FD2 UX Field Desk pós-critique | — (critique Impeccable; reusa dashboard + badges) | [field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md) | +| RS+ Escala e DRY pós-reset senha/perfil | — (sem design novo; reusa login + perfil + padrões de form de campanha) | [escala-dry-pos-reset-senha-perfil.md](plans/escala-dry-pos-reset-senha-perfil.md) | +| FD+ Escala e DRY pós-Field Desk | — (sem design novo; reusa shells/strips Field Desk) | [escala-dry-pos-field-desk.md](plans/escala-dry-pos-field-desk.md) | +| FD2 UX Field Desk pós-critique | — (critique Impeccable; reusa dashboard + badges) | [field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md) | | B5 Escala e DRY pós-B2 | — (sem design novo; reusa geometrias B2 + padrões de scripts CLI) | [escala-dry-pos-b2.md](plans/escala-dry-pos-b2.md) | | E6 Escala e DRY pós-E1+E3 | — (sem design novo; reusa Metas 2026 + overview B1) | [escala-dry-pos-e1.md](plans/escala-dry-pos-e1.md) | -| E7 Escala e DRY pós-E2 | — (sem design novo; reusa Baseline + Insights + overview tendência/conversão) | [escala-dry-pos-e2.md](plans/escala-dry-pos-e2.md) (Fase 2 ✓; Fases 1, 3, 4 pendentes) | +| E7 Escala e DRY pós-E2 | — (sem design novo; reusa Baseline + Insights + overview tendência/conversão) | [escala-dry-pos-e2.md](plans/escala-dry-pos-e2.md) (Fase 2 ✓; Fases 1, 3, 4 pendentes) | | D2 Notificações (sino) | `Notificacoes-PWA` (central de notificações) | [notifications.md](plans/notifications.md) | | D1 PWA `/campanha` | `Notificacoes-PWA` (banner "Instale o app da campanha") | [pwa-campanha.md](plans/pwa-campanha.md) | | C5 Operação dia D / GOTV | `Dia-D-GOTV` | sem plano ainda — o design é a primeira materialização do item (check-off de apoiadores por seção/zona, progresso "62% da base", filtro "Ainda não confirmados", estado "Sincronizado" offline-tolerante) | @@ -266,36 +266,36 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): **O0+** (ap **Janela 2 — 05/08 → 16/08 (pré-propaganda): base nominal + inteligência + agenda prontas para o arranque.** -| Ordem | Item | Plano | Depende de | Paralelizável com | -| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------- | ------------------------------------- | -| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta e mesclada em `main` 2026-07-18 — deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 | -| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–5 + simplify)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7, C8 | -| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 | -| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7, C8 | -| 11 | C7 Escala e DRY pós-C3 **(entregue 2026-07-19 — Fases 1–5; feed O(n) condicional fora de escopo)** | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 | -| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 | -| 13 | C9 Escala e DRY pós-C8 (filtro unificado Payload↔SQL, `contactSearchQuery` no aggregate, loader único `/apoiadores`, forms restantes) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c8.md) | C8 (merge) | A4, C3, C7, E1+E3, E2, C10 | -| 14 | C10 Escala e DRY pós-C9 (dedup access lista, scope único de núcleos do coordenador, `errorProps` nos forms) _(cortável se a base nominal permanecer pequena; Fase 3 DRY pode entrar isolada)_ | [detalhes](plans/escala-dry-pos-c9.md) | C9 (merge) | A4, C3, C7, E1+E3, E2, C11 | -| 15 | C11 Escala e DRY pós-C7 (feed O(n) condicional, loaders do detalhe, selects por aba, hot paths mutação/form, utilities de busca) _(cortável se a agenda permanecer pequena; Fase 1 migration só com volume medido)_ | [detalhes](plans/escala-dry-pos-c7.md) | C7 (merge) | A4, C3, C10, E1+E3, E2 | -| 16 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais **(implementado e mesclado em `main` 2026-07-19 — migration `20260719_054522`; `/simplify` → E6)** | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela | -| 17 | E6 Escala e DRY pós-E1+E3 (aggregate SQL metas/prioridade, geografia baseline, DRY formatação) _(cortável se poucos núcleos ativos)_ | [detalhes](plans/escala-dry-pos-e1.md) | E1+E3 ✓ | E2, C10, A7, E4 | -| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) **(implementado 2026-07-19; `/simplify` → E7)** | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, E7, C7, C8, C9, C10, C11 | -| 19 | E7 Escala e DRY pós-E2 (trend VM único no detalhe, int `loadNucleusListElectionOverview`, tipo `VoteTrendDistribution`, helpers wipe eleitoral nos int tests) _(Fase 2 int entregue 2026-07-19; Fases 1, 3 e 4 pendentes; F4 cortável — só DX de teste)_ | [detalhes](plans/escala-dry-pos-e2.md) | E2 (merge) | E6, A7, E4, C10 | +| Ordem | Item | Plano | Depende de | Paralelizável com | +| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------- | ----------------------------------- | +| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta e mesclada em `main` 2026-07-18 — deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 | +| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–5 + simplify)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7, C8 | +| 9 | A4 Baseline no produto + insight Gap vs 2022 **(implementado e mesclado em `main` 2026-07-18 — Fases 2–4 + simplify)** | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7, C8 | +| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7, C8 | +| 11 | C7 Escala e DRY pós-C3 **(entregue 2026-07-19 — Fases 1–5; feed O(n) condicional fora de escopo)** | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6, C8, E1+E3, E2 | +| 12 | C8 Escala e DRY pós-C6 (locks bulk em 1 RT, leituras/`pg_trgm`, helpers drizzle, forms DRY) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c6.md) | C6 (merge) | A4, C3, C7, E1+E3, E2, C9 | +| 13 | C9 Escala e DRY pós-C8 (filtro unificado Payload↔SQL, `contactSearchQuery` no aggregate, loader único `/apoiadores`, forms restantes) **(implementado e mesclado em `main` 2026-07-19 — Fases 1–4 + simplify)** _(cortável se a base nominal permanecer pequena)_ | [detalhes](plans/escala-dry-pos-c8.md) | C8 (merge) | A4, C3, C7, E1+E3, E2, C10 | +| 14 | C10 Escala e DRY pós-C9 (dedup access lista, scope único de núcleos do coordenador, `errorProps` nos forms) _(cortável se a base nominal permanecer pequena; Fase 3 DRY pode entrar isolada)_ | [detalhes](plans/escala-dry-pos-c9.md) | C9 (merge) | A4, C3, C7, E1+E3, E2, C11 | +| 15 | C11 Escala e DRY pós-C7 (feed O(n) condicional, loaders do detalhe, selects por aba, hot paths mutação/form, utilities de busca) _(cortável se a agenda permanecer pequena; Fase 1 migration só com volume medido)_ | [detalhes](plans/escala-dry-pos-c7.md) | C7 (merge) | A4, C3, C10, E1+E3, E2 | +| 16 | E1 Metas em cenários + prioridade + E3 dobradinhas/encaminhamentos manuais **(implementado e mesclado em `main` 2026-07-19 — migration `20260719_054522`; `/simplify` → E6)** | [detalhes](plans/mapa-projecao-municipios.md) | — | tudo da janela | +| 17 | E6 Escala e DRY pós-E1+E3 (aggregate SQL metas/prioridade, geografia baseline, DRY formatação) _(cortável se poucos núcleos ativos)_ | [detalhes](plans/escala-dry-pos-e1.md) | E1+E3 ✓ | E2, C10, A7, E4 | +| 18 | E2 Série histórica TSE 2014/2018 + tendência automática (seed por ano; sem migration) **(implementado 2026-07-19; `/simplify` → E7)** | [detalhes](plans/mapa-projecao-municipios.md) | A4 ✓ (suave: A7 F1 — mesmo loader) | E1+E3, E6, E7, C7, C8, C9, C10, C11 | +| 19 | E7 Escala e DRY pós-E2 (trend VM único no detalhe, int `loadNucleusListElectionOverview`, tipo `VoteTrendDistribution`, helpers wipe eleitoral nos int tests) _(Fase 2 int entregue 2026-07-19; Fases 1, 3 e 4 pendentes; F4 cortável — só DX de teste)_ | [detalhes](plans/escala-dry-pos-e2.md) | E2 (merge) | E6, A7, E4, C10 | **Janela 3 — 16/08 → set (campanha de rua): inteligência ampliada, visualização e engajamento.** -| Ordem | Item | Plano | Depende de | Paralelizável com | -| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- | -| 18 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 ✓ + E3 ✓ (suave: E2 ✓ — tendência visível na revisão; E6 F1 — perf da lista) | A5, A7, A8, B3, B5, C4, D2 | -| 19 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto; alavancagem inclui Fase 2 virada por vencedor **ou** share da direita) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | A7, A8, B3, B5, C4, E4, D1 | -| 20 | A8 Perfis do eleitorado — perfil médio IBGE (Censo) + manuais na aba Eleitorado _(cortável: manuais já existem)_ | [detalhes](plans/perfil-eleitorado-ibge.md) | A1 ✓ (suave: B2 ✓ — `codarea`) | A5, A7, B3, B5, C4, E4, D1 | -| 21 | A7 Escala e DRY pós-A4 (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético, batch federal flip/leverage na lista) _(Fase 1 agregar federal entregue 2026-07-19; Fases 2–5 pendentes; F4 pós-B3; F5 pós-A5 via `capture-review-debts`)_ | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, A8, B3, B5, B6, C4, E4, D1 | -| 22 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, A8, B5, B6, C4, E4, D1 | -| 23 | B5 Escala e DRY pós-B2 (cache CLI + factory mun/TI; F1 lazy ✓) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 entregue com B3) | A5, A7, A8, B3, B6, C4, E4, D1 | -| 24 | B6 Escala e DRY pós-B3 (`BahiaMap` setStyle incremental) _(cortável se troca de métrica for rara)_ | [detalhes](plans/escala-dry-pos-b3.md) | B3 (suave: A7 F4 na lista) | A5, A7, A8, B5, C4, E4, D1 | -| 25 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, A8, B3, B5, B6, E4, D1 | -| 26 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo | -| 27 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, A8, B3, B5, B6, C4, E4 | +| Ordem | Item | Plano | Depende de | Paralelizável com | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------ | +| 18 | E4 Import único da planilha (`pnpm db:seed:mapa`; sem criar `Contact`/`leadership`) | [detalhes](plans/mapa-projecao-municipios.md) | E1 ✓ + E3 ✓ (suave: E2 ✓ — tendência visível na revisão; E6 F1 — perf da lista) | A5, A7, A8, B3, B5, C4, D2 | +| 19 | A5 Insights derivados do baseline (5 itens, paralelizáveis entre si; conversão e classificação exigem limiares de produto; alavancagem inclui Fase 2 virada por vencedor **ou** share da direita) | [conversão](plans/insight-taxa-conversao.md) · [classificação](plans/insight-classificacao-territorial.md) · [alavancagem](plans/insight-alavancagem-chapa.md) · [mobilização](plans/insight-mobilizacao-brancos-nulos.md) · [competitiva](plans/insight-inteligencia-competitiva.md) | A4 | A7, A8, B3, B5, C4, E4, D1 | +| 20 | A8 Perfis do eleitorado — perfil médio IBGE (Censo) + manuais na aba Eleitorado _(cortável: manuais já existem)_ | [detalhes](plans/perfil-eleitorado-ibge.md) | A1 ✓ (suave: B2 ✓ — `codarea`) | A5, A7, B3, B5, C4, E4, D1 | +| 21 | A7 Escala e DRY pós-A4 (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético, batch federal flip/leverage na lista) _(Fase 1 agregar federal entregue 2026-07-19; Fases 2–5 pendentes; F4 pós-B3; F5 pós-A5 via `capture-review-debts`)_ | [detalhes](plans/escala-dry-pos-a4.md) | A4 ✓ (suave: A5/B3/E2 amplificam o custo) | A5, A8, B3, B5, B6, C4, E4, D1 | +| 22 | B2 Mapa Fase 1 (geometrias) **(entregue 2026-07-18)** + B3 Leaflet nas superfícies | [detalhes](plans/mapa-bahia-geometrias.md) | B3 ← B1 ✓ + B2 ✓ (suave: A4 coroplético; E1/E2 métricas; B5 F1 lazy geometrias) | A5, A7, A8, B5, B6, C4, E4, D1 | +| 23 | B5 Escala e DRY pós-B2 (cache CLI + factory mun/TI; F1 lazy ✓) | [detalhes](plans/escala-dry-pos-b2.md) | B2 ✓ (F1 entregue com B3) | A5, A7, A8, B3, B6, C4, E4, D1 | +| 24 | B6 Escala e DRY pós-B3 (`BahiaMap` setStyle incremental) _(cortável se troca de métrica for rara)_ | [detalhes](plans/escala-dry-pos-b3.md) | B3 (suave: A7 F4 na lista) | A5, A7, A8, B5, C4, E4, D1 | +| 25 | C4 Demandas | [detalhes](plans/demandas-campanha.md) | A1 (suave: C3 para a relação `actionPlan`) | A5, A7, A8, B3, B5, B6, E4, D1 | +| 26 | D1 PWA `/campanha` **(entregue antecipado em 2026-07-18)** | [detalhes](plans/pwa-campanha.md) | — | tudo | +| 27 | D2 Notificações (push + sino) — sino não depende do PWA; push sim | [detalhes](plans/notifications.md) | D1 ✓ (push) + chave `campanha-notificacoes-push` do lote jurídico | A5, A7, A8, B3, B5, B6, C4, E4 | **Janela 4 — set → 04/10 (reta final): dobradinha, dia D e estabilização.** @@ -312,7 +312,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): **O0+** (ap - **Visitados recentemente** ✓ (client-side, `localStorage`; MVP entregue 2026-07-19). → [detalhes](plans/visitados-recentemente.md) - **VR+ Escala e DRY pós-visitados recentemente** (refresh bfcache/storage, DRY tempo relativo, `CampaignLinkListRow`, shell do dashboard) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-visitados-recentemente.md) - **Reset de senha self-service + foto de perfil** ✓ (Resend + `/campanha/perfil`; MVP entregue 2026-07-19). → [detalhes](plans/reset-senha-foto-perfil.md) -- **RS+ Escala e DRY pós-reset senha/perfil** (auth read `depth: 0`, avatar fora da TX, **`CampaignAuthPageShell` ✓ parcial**, schemas de senha unificados, login em `CampaignFormActionState`) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-reset-senha-perfil.md) +- **RS+ Escala e DRY pós-reset senha/perfil** (auth read `depth: 0`, avatar fora da TX, **auth UI parcial ✓** — shell/card/logo + critique login fechados; pendem `CampaignPasswordFields` + login em `CampaignFormActionState`) — engenharia pós-MVP; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-reset-senha-perfil.md) - **Field Desk polish** (Impeccable quieter→polish; critique 28→32) — shells/strips/freshness/filas-first/Mais filtros/empties; em branch local 2026-07-19. → `PRODUCT.md` / `DESIGN.md` - **FD+ Escala e DRY pós-Field Desk** (`CampaignPageShell` size, heading class, VoteGoals→MetricStrip, datas canônicas, NucleusFilters bag) — engenharia pós-polish; paralelizável com qualquer janela. → [detalhes](plans/escala-dry-pos-field-desk.md) - **FD2 UX Field Desk pós-critique** (bulk na fila Sem coordenador, glossário inline, outline h2 Filas, empty coordenador+CTA, Sidebar motion cortável) — produto/UX pós-critique; _(validar prioridades com produto)_. → [detalhes](plans/field-desk-ux-pos-critique.md) diff --git a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx index f33b31e0..e9197fa6 100644 --- a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx +++ b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx @@ -1,17 +1,18 @@ 'use client' -import Link from 'next/link' import { useActionState } from 'react' import { requestCampaignPasswordResetFormAction } from '@/app/(campaign)/campanha/actions/password' +import { CampaignAuthBackToLoginLink } from '@/components/campaign/CampaignAuthBackToLoginLink' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT, - campaignAuthHeadingClassName, + campaignAuthMutedTextClassName, } from '@/lib/campaignAuthCopy' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' import { fieldError } from '@/utilities/campaignFormFields' @@ -21,20 +22,21 @@ export const ForgotPasswordForm = () => { requestCampaignPasswordResetFormAction, {} satisfies CampaignFormActionState, ) + const emailError = fieldError(state.fieldErrors, 'email') return ( - -

Esqueceu a senha?

- - Informe o e-mail cadastrado na sua conta. {CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT} - -
+ Informe o e-mail cadastrado na sua conta. {CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT} + } + /> {state.status === 'success' && state.message ? ( -

+

{state.message} -

+
) : (
@@ -51,21 +53,12 @@ export const ForgotPasswordForm = () => { /> {state.message ? {state.message} : null} - {fieldError(state.fieldErrors, 'email') ? ( - {fieldError(state.fieldErrors, 'email')} - ) : null} + {emailError ? {emailError} : null} -

- - Voltar ao login - -

+
)} diff --git a/src/app/(campaign)/campanha/login/LoginForm.tsx b/src/app/(campaign)/campanha/login/LoginForm.tsx index 8b1a689e..f6ebfe6c 100644 --- a/src/app/(campaign)/campanha/login/LoginForm.tsx +++ b/src/app/(campaign)/campanha/login/LoginForm.tsx @@ -4,15 +4,20 @@ import Link from 'next/link' import { useActionState, useState } from 'react' import { loginCampaignFormAction, type LoginResult } from '@/app/(campaign)/campanha/actions/auth' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' import { + CAMPAIGN_FIRST_ACCESS_HINT, CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT, - campaignAuthHeadingClassName, + CAMPAIGN_LOGIN_SUBTITLE, + campaignAuthMutedTextClassName, + campaignAuthTextLinkClassName, } from '@/lib/campaignAuthCopy' +import { cn } from '@/lib/utils' const LOGIN_ERROR_ID = 'login-credentials-error' @@ -35,13 +40,7 @@ export const LoginForm = () => { return ( - -

Entrar na campanha

- Use o e-mail da equipe ou o celular da liderança. -

- Primeiro acesso? Peça um convite no WhatsApp ao coordenador do seu núcleo. -

-
+
@@ -78,16 +77,13 @@ export const LoginForm = () => { />
- + Esqueceu a senha? {hasAuthError ? ( -

+

{CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT} -

+
) : null}
@@ -97,6 +93,19 @@ export const LoginForm = () => { {pending ? 'Entrando...' : 'Entrar'} +
+ + Primeiro acesso? + +
+ {CAMPAIGN_FIRST_ACCESS_HINT} +
+
diff --git a/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx b/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx index a75213fc..98983a77 100644 --- a/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx +++ b/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx @@ -1,15 +1,15 @@ 'use client' -import Link from 'next/link' import { useActionState } from 'react' import { resetCampaignPasswordFormAction } from '@/app/(campaign)/campanha/actions/password' +import { CampaignAuthBackToLoginLink } from '@/components/campaign/CampaignAuthBackToLoginLink' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' -import { campaignAuthHeadingClassName } from '@/lib/campaignAuthCopy' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' import { fieldError } from '@/utilities/campaignFormFields' @@ -18,13 +18,15 @@ export const ResetPasswordForm = ({ token }: { token: string }) => { resetCampaignPasswordFormAction.bind(null, token), {} satisfies CampaignFormActionState, ) + const passwordError = fieldError(state.fieldErrors, 'password') + const passwordConfirmationError = fieldError(state.fieldErrors, 'passwordConfirmation') return ( - -

Redefinir senha

- Escolha uma nova senha com pelo menos 8 caracteres. -
+
@@ -53,24 +55,15 @@ export const ResetPasswordForm = ({ token }: { token: string }) => { /> {state.message ? {state.message} : null} - {fieldError(state.fieldErrors, 'password') ? ( - {fieldError(state.fieldErrors, 'password')} - ) : null} - {fieldError(state.fieldErrors, 'passwordConfirmation') ? ( - {fieldError(state.fieldErrors, 'passwordConfirmation')} + {passwordError ? {passwordError} : null} + {passwordConfirmationError ? ( + {passwordConfirmationError} ) : null} -

- - Voltar ao login - -

+
diff --git a/src/app/(campaign)/campanha/redefinir-senha/page.tsx b/src/app/(campaign)/campanha/redefinir-senha/page.tsx index 6efb1004..d2a90d71 100644 --- a/src/app/(campaign)/campanha/redefinir-senha/page.tsx +++ b/src/app/(campaign)/campanha/redefinir-senha/page.tsx @@ -3,10 +3,10 @@ import Link from 'next/link' import { redirect } from 'next/navigation' import { ResetPasswordForm } from '@/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm' +import { CampaignAuthCardHeader } from '@/components/campaign/CampaignAuthCardHeader' import { CampaignAuthPageShell } from '@/components/campaign/CampaignAuthPageShell' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' -import { campaignAuthHeadingClassName } from '@/lib/campaignAuthCopy' +import { Card, CardContent } from '@/components/ui/card' import { CAMPAIGN_PASSWORD_RESET_TOKEN_MIN_LENGTH } from '@/lib/schemas/campaignPassword' import { getCampaignUser } from '@/utilities/campaignAuth' import { CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE } from '@/utilities/campaignPasswordReset' @@ -37,10 +37,10 @@ export default async function CampaignResetPasswordPage({ return ( - -

Link inválido

- {CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE} -
+