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.
registerTypepassa deautoUpdateparaprompt: 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 callbackonRegisteredSWpara agendar a checagem periódica (setIntervalchamandoregistration.update()), com o intervalo em constante.- Componente novo
PwaUpdateBanner(ex.:src/components/pwa-update-banner/): usa o hookuseRegisterSWdevirtual:pwa-register/react; lêneedRefreshe a funçãoupdateServiceWorker; 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/clientnostypesdotsconfig(ou umvite-env.d.tscom/// <reference types="vite-plugin-pwa/react" />) para o importvirtual:pwa-register/reacttipar. - 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-bannerausente) — 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
needRefreshpara 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 noonRegisteredSWdo hook - [x]
apps/web— componentePwaUpdateBanner(useRegisterSW), comdata-testid, montado no layout raiz; responsivo desktop + mobile, empurra o conteúdo - [x]
apps/web— i18n PT/EN; typesvite-plugin-pwa/react; depworkbox-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 hookuseRegisterSW; sem isso haveria registro duplo).src/components/pwa-update-banner/index.tsx: banner fixo no topo comuseRegisterSW(needRefresh/updateServiceWorker), botões Atualizar e Depois, checagem periódica de 30 min viaonRegisteredSW, epadding-topno body enquanto visível (RN6 — empurra, não cobre).data-testid:pwa-update-banner/-refresh/-dismiss.- Montado no
App.tsx(dentro de<Refine>, junto aoRefineKbar) → vale em qualquer tela, inclusive auth. - i18n PT/EN (
pwaUpdate.message/refresh/dismiss);vite-env.d.tsreferenciavite-plugin-pwa/react.
Divergências / gotchas do plano:
workbox-windowcomo dependência direta (não estava no plano): o módulo virtualvirtual:pwa-register/reactimportaworkbox-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: opnpm add(pnpm v11) criou umapps/web/pnpm-workspace.yamlsó comallowBuilds(sempackages:); oCOPY apps/web/levava ao container e opnpm buildquebrava com "packages field missing or empty". Removido. (Local não pegava porque o build chama os binários direto, nãopnpm build.)- A checagem periódica ficou no
onRegisteredSWdo hook (não novite.config), que é onde se tem aregistration.
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.