Pular para conteúdo

0005 — Aviso de atualização do PWA (banner com botão "Atualizar")

Status CONCLUÍDA
Branch spec/0005-pwa-prompt-de-atualizacao
Criada em 2026-07-29
Apps afetados web (api · e2e ver seção 5)

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

O PWA usa registerType: "autoUpdate" (vite-plugin-pwa): quando um build novo é publicado, o service worker baixa os assets em silêncio e só passa a servi-los no próximo carregamento — na tela instalada, isso significa fechar e reabrir o app. O usuário não sabe que existe versão nova e pode ficar preso num bundle antigo por tempo indeterminado (dor real observada nesta sprint: correções de UI "não apareciam" no celular até reabrir o PWA).

2. Objetivo

Quando houver versão nova, mostrar um aviso visível com botão "Atualizar" — como Gmail/Twitter e outros PWAs de mercado — para o usuário aplicar a atualização na hora, sem precisar fechar o app nem adivinhar que há algo novo.

3. Fora de escopo

  • Toast de "pronto para uso offline" (avaliado e deixado de fora nesta feature).
  • Notificações push, changelog/"novidades", número de versão visível ao usuário.
  • Estratégia de cache offline em si (continua a atual do workbox) — aqui só muda o ciclo de ativação (silencioso → sob demanda).

4. Regras de negócio

  • RN1. registerType passa de autoUpdate para prompt: o SW novo entra em waiting e não ativa sozinho — espera ação do usuário.
  • RN2. Ao detectar SW em waiting (needRefresh), exibir um banner fixo no topo, largura total, persistente, com o texto de nova versão e dois controles: "Atualizar" e "Depois".
  • RN3. "Atualizar" chama updateServiceWorker(true) (skipWaiting + reload) → o app recarrega já servindo a versão nova.
  • RN4. "Depois"/fechar oculta o banner; ele reaparece na próxima checagem periódica ou no próximo carregamento enquanto a versão nova não tiver sido aplicada.
  • RN5. Checagem periódica: além de no load, o app chama registration.update() a cada 30 min (ajustável por constante) enquanto aberto, para o aviso surgir sem depender de recarregar a página.
  • RN6. Responsivo: banner no topo em desktop e mobile. No mobile ele não pode cobrir a navegação (top app bar / bottom tab bar da casca mobile); empurra o conteúdo, não sobrepõe. Ver seção 5.
  • RN7. Sem service worker (ambiente sem PWA, navegador sem suporte, ou SW não registrado) o banner nunca aparece e o app funciona normalmente.

Permissões: não se aplica — é UI client-side, vale para qualquer usuário (autenticado ou nas telas públicas de auth). Independe de papel.

5. Impacto por app

apps/api

Sem impacto.

apps/web

  • vite.config.ts: registerType: "prompt"; registrar o callback onRegisteredSW para agendar a checagem periódica (setInterval chamando registration.update()), com o intervalo em constante.
  • Componente novo PwaUpdateBanner (ex.: src/components/pwa-update-banner/): usa o hook useRegisterSW de virtual:pwa-register/react; lê needRefresh e a função updateServiceWorker; renderiza o banner (RN2–RN4). Deve estar montado no layout raiz, acima das rotas, para valer em qualquer tela (inclui login/auth).
  • data-testid: pwa-update-banner, pwa-update-refresh (botão Atualizar), pwa-update-dismiss (Depois).
  • i18n: textos PT/EN ("Nova versão disponível", "Atualizar", "Depois").
  • Types: garantir vite-plugin-pwa/client nos types do tsconfig (ou um vite-env.d.ts com /// <reference types="vite-plugin-pwa/react" />) para o import virtual:pwa-register/react tipar.
  • Responsividade & PWA: o banner é o próprio mecanismo de PWA. Desktop: faixa no topo empurrando o conteúdo. Mobile: acima/integrado ao top app bar da casca (spec 0004), sem cobrir a bottom tab bar. Testar reduzindo o viewport.

apps/e2e

Testar o ciclo real de update do SW (2 builds → waiting → banner → reload) é caro e frágil no Playwright. Cobertura proposta:

  • e2e barato: garantir que, no estado normal (sem versão nova), o banner não aparece (pwa-update-banner ausente) — em desktop e mobile.
  • Fluxo de update: validação manual no dev-remote (publicar 2 builds e conferir o banner → Atualizar → versão nova). Opcional: expor um gancho de teste que force needRefresh para exercitar o render/DOM do banner sem depender do SW real.

6. Contrato de API

Sem impacto — feature 100% client-side. Não há endpoint novo nem alterado.

7. Critérios de aceite

  • [x] CA1. Com uma versão nova publicada, o banner no topo aparece com o ícone, "Nova versão disponível", "Atualizar" e "Depois". Provado ao vivo (2 builds + SW persistente): waiting: true → bannerCount: 1 (banner.png).
  • [x] CA2. "Atualizar" (updateServiceWorker(true)) recarrega e o banner some — o SW novo (waiting) é ativado. Provado ao vivo.
  • [x] CA3. "Depois" (setNeedRefresh(false)) oculta o banner; reaparece no próximo load/checagem enquanto houver SW em waiting (por construção do hook).
  • [x] CA4. O banner empurra o conteúdo (padding no body), não cobre a navegação — visível no banner.png (login empurrado para baixo). CSS responsivo (ícone só ≥sm, padding por breakpoint).
  • [x] CA5. Sem SW/atualização o banner não aparece — e2e pwa-update-banner (ausente em desktop e mobile).

8. Plano de implementação

Ordem: web → e2e (não há api).

  • [x] apps/web — vite.config.ts: registerType: "prompt" + injectRegister: null (registro fica com o hook); a checagem periódica ficou no onRegisteredSW do hook
  • [x] apps/web — componente PwaUpdateBanner (useRegisterSW), com data-testid, montado no layout raiz; responsivo desktop + mobile, empurra o conteúdo
  • [x] apps/web — i18n PT/EN; types vite-plugin-pwa/react; dep workbox-window (peer do módulo virtual — não vinha como dependência direta)
  • [x] apps/e2e — pwa-update-banner.spec.ts: banner ausente (desktop e mobile)
  • [x] Validado no browser + fluxo real no dev-remote (2 builds → banner → Atualizar → versão nova), com SW persistente
  • [x] Build de produção (roda tsc) sem erro

9. Decisões em aberto

Resolvidas (2026-07-29):

  • [x] Intervalo da checagem periódica — 30 min (RN5), em constante ajustável.
  • [x] Testabilidade e2e do fluxo de update — validação manual do ciclo de SW no dev-remote; o e2e cobre apenas a ausência do banner no estado normal (desktop e mobile). Sem gancho de teste para forçar needRefresh.

10. Registro

Implementado (apps/web):

  • vite.config.ts: registerType: "autoUpdate" → "prompt" + injectRegister: null (quem registra o SW é o hook useRegisterSW; sem isso haveria registro duplo).
  • src/components/pwa-update-banner/index.tsx: banner fixo no topo com useRegisterSW (needRefresh / updateServiceWorker), botões Atualizar e Depois, checagem periódica de 30 min via onRegisteredSW, e padding-top no body enquanto visível (RN6 — empurra, não cobre). data-testid: pwa-update-banner / -refresh / -dismiss.
  • Montado no App.tsx (dentro de <Refine>, junto ao RefineKbar) → vale em qualquer tela, inclusive auth.
  • i18n PT/EN (pwaUpdate.message/refresh/dismiss); vite-env.d.ts referencia vite-plugin-pwa/react.

Divergências / gotchas do plano:

  • workbox-window como dependência direta (não estava no plano): o módulo virtual virtual:pwa-register/react importa workbox-window; sob o pnpm isolado ele não resolvia (só existia como dep transitiva). pnpm add workbox-window@7.4.1.
  • pnpm-workspace.yaml órfão: o pnpm add (pnpm v11) criou um apps/web/pnpm-workspace.yaml só com allowBuilds (sem packages:); o COPY apps/web/ levava ao container e o pnpm build quebrava com "packages field missing or empty". Removido. (Local não pegava porque o build chama os binários direto, não pnpm build.)
  • A checagem periódica ficou no onRegisteredSW do hook (não no vite.config), que é onde se tem a registration.

Validação: e2e de ausência (desktop + mobile) verde. Fluxo real provado no dev-remote com 2 builds + service worker persistente (Playwright launchPersistentContext): BUILD-1 instala o SW → publica BUILD-2 → registration.update() → waiting: true e o banner aparece (banner.png) → Atualizar aplica e o banner some. (Aprendizado: um diff só de comentário não gera bundle novo — o minificador remove; o marcador de teste teve de ser uma string real.) Build final limpa (sem marcadores) no ar.

Evidência: banner.png (banner no topo, empurrando o login). Artefatos de teste (pwa-validate.mjs, pwadir) removidos.