From 5bfa226a7e055d4a498cdb23717ec4daf12ac29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sat, 18 Jul 2026 17:28:34 -0300 Subject: [PATCH] Add installable PWA foundation for the /campanha field app. Scoped manifest and service worker unlock offline shell and future push, with an install toast and logout cache wipe. --- public/campaign-icons/apple-touch-icon.png | Bin 0 -> 2495 bytes public/campaign-icons/icon-192.png | Bin 0 -> 2550 bytes public/campaign-icons/icon-512.png | Bin 0 -> 7731 bytes public/campaign-icons/icon-maskable-512.png | Bin 0 -> 7156 bytes src/app/(campaign)/campanha/(app)/layout.tsx | 2 + .../campanha/manifest.webmanifest/route.ts | 14 ++ src/app/(campaign)/campanha/offline/page.tsx | 35 ++++ src/app/(campaign)/campanha/sw.js/route.ts | 22 ++ src/app/(campaign)/layout.tsx | 18 +- src/components/campaign/CampaignSidebar.tsx | 10 +- src/components/campaign/InstallPwaToast.tsx | 150 ++++++++++++++ .../campaign/RegisterServiceWorker.tsx | 20 ++ src/utilities/campaignPwa.ts | 191 ++++++++++++++++++ src/utilities/campaignPwaClient.ts | 28 +++ tests/e2e/campaign-pwa.e2e.spec.ts | 35 ++++ tests/unit/campaignPwa.unit.spec.ts | 71 +++++++ 16 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 public/campaign-icons/apple-touch-icon.png create mode 100644 public/campaign-icons/icon-192.png create mode 100644 public/campaign-icons/icon-512.png create mode 100644 public/campaign-icons/icon-maskable-512.png create mode 100644 src/app/(campaign)/campanha/manifest.webmanifest/route.ts create mode 100644 src/app/(campaign)/campanha/offline/page.tsx create mode 100644 src/app/(campaign)/campanha/sw.js/route.ts create mode 100644 src/components/campaign/InstallPwaToast.tsx create mode 100644 src/components/campaign/RegisterServiceWorker.tsx create mode 100644 src/utilities/campaignPwa.ts create mode 100644 src/utilities/campaignPwaClient.ts create mode 100644 tests/e2e/campaign-pwa.e2e.spec.ts create mode 100644 tests/unit/campaignPwa.unit.spec.ts diff --git a/public/campaign-icons/apple-touch-icon.png b/public/campaign-icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad1be1b33f4c193494b3037fa38789ed7925e41 GIT binary patch literal 2495 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 literal 0 HcmV?d00001 diff --git a/public/campaign-icons/icon-512.png b/public/campaign-icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..3f73342b9aadd47bd2a0a1ec657bb0dc196f1d37 GIT binary patch literal 7731 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 literal 0 HcmV?d00001 diff --git a/public/campaign-icons/icon-maskable-512.png b/public/campaign-icons/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..f149edb51fa34625e0075710681e9373d79d2f14 GIT binary patch literal 7156 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 BG-v<- literal 0 HcmV?d00001 diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index 0ed58918..08b70ce4 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -3,6 +3,7 @@ import React from 'react' import { CampaignBottomNav } from '@/components/campaign/CampaignBottomNav' import { CampaignSidebar } from '@/components/campaign/CampaignSidebar' +import { InstallPwaToast } from '@/components/campaign/InstallPwaToast' import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/Sidebar' import { Toaster } from '@/components/ui/Toaster' import { getCampaignUser } from '@/utilities/campaignAuth' @@ -38,6 +39,7 @@ export default async function CampaignAppLayout({ children }: { children: React. + ) diff --git a/src/app/(campaign)/campanha/manifest.webmanifest/route.ts b/src/app/(campaign)/campanha/manifest.webmanifest/route.ts new file mode 100644 index 00000000..ffc2b243 --- /dev/null +++ b/src/app/(campaign)/campanha/manifest.webmanifest/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server' + +import { CAMPAIGN_WEB_MANIFEST } from '@/utilities/campaignPwa' + +export const dynamic = 'force-static' + +export function GET(): NextResponse { + return NextResponse.json(CAMPAIGN_WEB_MANIFEST, { + headers: { + 'Content-Type': 'application/manifest+json; charset=utf-8', + 'Cache-Control': 'no-cache', + }, + }) +} diff --git a/src/app/(campaign)/campanha/offline/page.tsx b/src/app/(campaign)/campanha/offline/page.tsx new file mode 100644 index 00000000..d5f01ecf --- /dev/null +++ b/src/app/(campaign)/campanha/offline/page.tsx @@ -0,0 +1,35 @@ +import { WifiOff } from 'lucide-react' +import type { Metadata } from 'next' +import Link from 'next/link' + +import { Button } from '@/components/ui/button' + +export const metadata: Metadata = { + title: 'Offline | Campanha', + robots: { + index: false, + follow: false, + }, +} + +export default function CampaignOfflinePage() { + return ( +
+
+
+
+
+

Você está offline

+

+ Sem conexão no momento. Abra novamente quando a rede voltar, ou tente uma página que + você já visitou nesta sessão. +

+
+ +
+
+ ) +} diff --git a/src/app/(campaign)/campanha/sw.js/route.ts b/src/app/(campaign)/campanha/sw.js/route.ts new file mode 100644 index 00000000..ac8523a2 --- /dev/null +++ b/src/app/(campaign)/campanha/sw.js/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server' + +import { + buildCampaignServiceWorkerScript, + CAMPAIGN_PWA_SCOPE, + resolveCampaignPwaBuildId, +} from '@/utilities/campaignPwa' + +// Built once per deploy; build id in the script body still invalidates SW updates. +export const dynamic = 'force-static' + +export function GET(): NextResponse { + const script = buildCampaignServiceWorkerScript(resolveCampaignPwaBuildId()) + + return new NextResponse(script, { + headers: { + 'Content-Type': 'text/javascript; charset=utf-8', + 'Cache-Control': 'no-cache', + 'Service-Worker-Allowed': CAMPAIGN_PWA_SCOPE, + }, + }) +} diff --git a/src/app/(campaign)/layout.tsx b/src/app/(campaign)/layout.tsx index 0f7d9d84..01962fe3 100644 --- a/src/app/(campaign)/layout.tsx +++ b/src/app/(campaign)/layout.tsx @@ -1,8 +1,10 @@ -import type { Metadata } from 'next' +import type { Metadata, Viewport } from 'next' import { Inter } from 'next/font/google' import React from 'react' +import { RegisterServiceWorker } from '@/components/campaign/RegisterServiceWorker' import { VoteEstimateFocusProvider } from '@/components/campaign/VoteEstimateFocusProvider' +import { CAMPAIGN_PWA_MANIFEST_PATH, CAMPAIGN_PWA_THEME_COLOR } from '@/utilities/campaignPwa' import '../(frontend)/styles.css' @@ -14,6 +16,19 @@ export const metadata: Metadata = { index: false, follow: false, }, + manifest: CAMPAIGN_PWA_MANIFEST_PATH, + appleWebApp: { + capable: true, + title: 'Campanha', + statusBarStyle: 'default', + }, + icons: { + apple: '/campaign-icons/apple-touch-icon.png', + }, +} + +export const viewport: Viewport = { + themeColor: CAMPAIGN_PWA_THEME_COLOR, } export default function CampanhaRootLayout({ children }: { children: React.ReactNode }) { @@ -26,6 +41,7 @@ export default function CampanhaRootLayout({ children }: { children: React.React > {children} + ) diff --git a/src/components/campaign/CampaignSidebar.tsx b/src/components/campaign/CampaignSidebar.tsx index 528bece8..c79abda5 100644 --- a/src/components/campaign/CampaignSidebar.tsx +++ b/src/components/campaign/CampaignSidebar.tsx @@ -1,5 +1,6 @@ 'use client' +import type { FormEvent } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' @@ -21,6 +22,7 @@ import { useSidebar, } from '@/components/ui/Sidebar' import type { CampaignUser } from '@/payload-types' +import { clearCampaignPwaCaches } from '@/utilities/campaignPwaClient' export type CampaignSidebarUser = { name: string @@ -38,6 +40,12 @@ export const CampaignSidebar = ({ user }: { user: CampaignSidebarUser }) => { const pathname = usePathname() const { isMobile, setOpenMobile } = useSidebar() + const handleLogout = async (event: FormEvent) => { + event.preventDefault() + await clearCampaignPwaCaches() + await logoutCampaign() + } + return ( @@ -82,7 +90,7 @@ export const CampaignSidebar = ({ user }: { user: CampaignSidebarUser }) => { {user.email} ) : null} -
+