-
Notifications
You must be signed in to change notification settings - Fork 44
feat: add progressive web app implementation #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: add progressive web app implementation #125
Conversation
…ompt - Add PWA configuration in next.config.ts using next-pwa - Create custom service worker (sw-custom.js) for caching and push notifications - Implement PWA install prompt component for user experience - Add manifest file for PWA metadata and icons - Include offline.html for offline access - Update layout and metadata for PWA compatibility - Add hooks for PWA state management and notifications - Introduce new icons for PWA in public/icons directory
…ion and service worker files - Update next.config.ts and sw-custom.js to use consistent double quotes for strings - Refactor layout.tsx and manifest.ts for improved readability with multiline strings - Adjust PWAInstallPrompt and PWAStatus components for better code organization and clarity - Enhance usePWA hook with consistent formatting and improved event handling
WalkthroughAdds PWA support: configures next-pwa in Next.js, registers a custom service worker, provides offline page and icons, defines a web app manifest, injects PWA meta in layout, and introduces hooks/components for install prompts and status. Also extends payout creation payload with additional default fields. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant App as Next.js App
participant Hook as usePWA()
participant SW as Service Worker
participant Net as Network
U->>App: Load page
App->>Hook: Initialize PWA
Hook->>SW: navigator.serviceWorker.register('/sw-custom.js')
SW-->>Hook: registration + updatefound events
alt Update available
SW-->>Hook: statechange -> installed (waiting)
Hook-->>App: hasUpdate = true
else No update
Hook-->>App: isSupported / isOnline / isInstalled
end
App-->>U: Show PWAStatus / PWAInstallPrompt
U->>App: Click "Update"
App->>Hook: updateServiceWorker()
Hook->>SW: postMessage('SKIP_WAITING')
SW-->>App: controllerchange
App->>U: Reload
sequenceDiagram
participant SW as Service Worker
participant Client as Page
participant Cache as Cache Storage
participant Net as Network
Note over SW: Install
SW->>Cache: Add offline.html, icons, root
SW->>SW: skipWaiting()
Note over SW: Activate
SW->>Cache: Delete old caches
SW->>Client: clients.claim()
Note over SW: Fetch
Client->>SW: GET request
alt navigation
SW->>Net: fetch()
alt net fail
SW->>Cache: match(offline.html)
SW-->>Client: offline.html
else success
SW-->>Client: response
end
else /api/*
SW->>Net: fetch()
alt success
SW->>Cache: put()
SW-->>Client: response
else fail
SW->>Cache: match()
SW-->>Client: cached or error
end
else static assets
SW->>Cache: match()
alt hit
SW-->>Client: cached
else miss
SW->>Net: fetch() then cache
SW-->>Client: response
end
end
sequenceDiagram
actor U as User
participant App as PWAInstallPrompt
participant Hook as usePWA()
participant SW as Service Worker
App-->>U: Show install/update prompt (if eligible)
U->>App: Click "Install"
App->>App: deferredPrompt.prompt()
App-->>U: Await choice
alt accepted
App->>App: Clear deferredPrompt
else dismissed
App->>App: Store suppression timestamp (7 days)
end
alt Update available
U->>App: Click "Update"
App->>Hook: updateServiceWorker()
Hook->>SW: postMessage('SKIP_WAITING')
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/modules/payouts/hooks/usePayoutMutations.ts (1)
55-69: Restore form values when creating a payout.These new defaults overwrite whatever the user submits (e.g., notes, deadlines, social links) because we spread
restDatafirst and then unconditionally clobber the same fields. Any non-empty input will be dropped on create, so the API receivesnullinstead. Please default only when the field is missing.- rewards: null, - skills: [], - social_media: null, - application_deadline: null, - announcement_deadline: null, - notes: null, - files: null, + rewards: data.rewards ?? null, + skills: data.skills ?? [], + social_media: + data.social_media !== undefined ? data.social_media : null, + application_deadline: + data.application_deadline !== undefined + ? data.application_deadline + : null, + announcement_deadline: + data.announcement_deadline !== undefined + ? data.announcement_deadline + : null, + notes: data.notes ?? null, + files: data.files ?? null,
🧹 Nitpick comments (12)
next.config.ts (1)
67-110: Review PWA caching strategies for potential performance issues.The runtime caching configuration has some concerns:
- Line 76: The catch-all pattern
/^https?.*/with NetworkFirst could cache external resources indiscriminately- Lines 81-82: 24-hour cache expiration for network requests may be too long for dynamic content
- Lines 88-97: Image caching strategy looks appropriate
Consider these improvements:
const pwaConfig = withPWA({ dest: "public", register: true, skipWaiting: true, disable: process.env.NODE_ENV === "development", buildExcludes: [/middleware-manifest\.json$/], sw: "sw-custom.js", // Use our custom service worker runtimeCaching: [ { - urlPattern: /^https?.*/, + urlPattern: ({ url }) => url.origin === self.location.origin, handler: "NetworkFirst", options: { cacheName: "offlineCache", expiration: { maxEntries: 200, - maxAgeSeconds: 24 * 60 * 60, // 24 hours + maxAgeSeconds: 60 * 60, // 1 hour }, networkTimeoutSeconds: 10, }, },This limits caching to same-origin requests and reduces cache duration for dynamic content.
package.json (1)
59-59: Migrate to @serwist/next for maintained PWA support
Replace next-pwa (v5.6.0) with @serwist/next (v9.2.1), as recommended by both the original maintainer and community fork; alternatively, use Next.js’s native PWA approach per the official docs.src/components/shared/PWAInstallPrompt.tsx (4)
53-70: Record suppression on OS prompt dismissal to honor the 7‑day cooldownIf the user dismisses the browser’s install prompt, we should respect the same cooldown used by the “Not now” button.
Apply this diff:
// Wait for the user to respond to the prompt const { outcome } = await deferredPrompt.userChoice; if (outcome === "accepted") { console.log("User accepted the install prompt"); } else { console.log("User dismissed the install prompt"); + // Suppress further prompts for 7 days + localStorage.setItem("pwa-install-dismissed", Date.now().toString()); } // Clear the deferredPrompt so it can only be used once setDeferredPrompt(null);
144-167: Improve accessibility: use dialog semantics and labelsTreat these as lightweight dialogs so screen readers announce them and users can identify the title.
Apply these minimal changes within each prompt block:
- Add role and labeling on the container.
- Add an id to the heading and reference it.
Example (apply similarly to all three prompts):
-<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm"> +<div + className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm" + role="dialog" + aria-modal="false" + aria-labelledby="pwa-install-title" +> <div className="rounded-lg border bg-background p-4 shadow-lg"> <div className="flex items-start justify-between"> <div className="flex-1"> - <h3 className="text-sm font-semibold">Install GrantFox</h3> + <h3 id="pwa-install-title" className="text-sm font-semibold">Install GrantFox</h3>Optional: add onKeyDown handlers to close on Escape.
Also applies to: 186-227, 82-123
131-141: Avoid reading localStorage during render; derive a memoized/single source of truthAccessing localStorage on every render is unnecessary and brittle. Load once and derive booleans.
Consider:
const [dismissedAt, setDismissedAt] = useState<number | null>(null); useEffect(() => { const t = localStorage.getItem("pwa-install-dismissed"); setDismissedAt(t ? Number.parseInt(t) : null); }, []); const installSuppressed = dismissedAt !== null && (Date.now() - dismissedAt) / (1000 * 60 * 60 * 24) < 7;Then replace both render-time reads with
installSuppressed.Also applies to: 175-183
35-39: iOS detection can be brittle on newer iPads (desktop UA)UA sniffing often misclassifies iPadOS. Prefer feature checks when possible.
Example:
const isIOSDevice = typeof navigator !== "undefined" && (/iPhone|iPad|iPod/.test(navigator.userAgent) || // iPadOS 13+ reports as Mac; check touch support (navigator.platform === "MacIntel" && (navigator as any).maxTouchPoints > 1)); setIsIOS(!!isIOSDevice);public/sw-custom.js (6)
71-89: Broaden success check toresponse.okand guard cache putsStatus 200 only misses 2xx range and can throw if
responseis undefined. Wrapcache.putwithresponse.ok.- .then((response) => { - // Cache successful responses - if (response.status === 200) { + .then((response) => { + // Cache successful responses + if (response && response.ok) { const responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseClone); }); } return response; })Apply the same
response && response.okpattern in Lines 104-110 and 122-127.
92-115: Static asset detection misses common formats; prefer destination checksRely on
request.destination(style, script, font, image) and extend formats like webp/avif/ico/json.- if ( - event.request.destination === "image" || - event.request.url.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/) - ) { + if ( + ["style", "script", "font", "image"].includes(event.request.destination) || + event.request.url.match(/\.(css|js|png|jpg|jpeg|gif|svg|webp|avif|ico|woff|woff2|json|map)$/) + ) {
60-68: Use navigation preload to improve first-load latencyIf supported, use
event.preloadResponsebefore falling back to network or offline.- if (event.request.mode === "navigate") { - event.respondWith( - fetch(event.request).catch(() => { - return caches.match(OFFLINE_URL); - }), - ); + if (event.request.mode === "navigate") { + event.respondWith((async () => { + try { + const preload = 'preloadResponse' in event ? await event.preloadResponse : null; + if (preload) return preload; + return await fetch(event.request); + } catch { + return caches.match(OFFLINE_URL); + } + })()); return; }And during activate, enable preload:
self.addEventListener("activate", (event) => { console.log("Service Worker: Activate event"); event.waitUntil( - caches.keys().then((cacheNames) => { + (async () => { + if ('navigationPreload' in self.registration) { + try { await self.registration.navigationPreload.enable(); } catch {} + } + const cacheNames = await caches.keys(); return Promise.all( - cacheNames.map((cacheName) => { + cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log("Service Worker: Deleting old cache:", cacheName); return caches.delete(cacheName); } - }), - ); - }), + return Promise.resolve(); + }) + ); + })(), );
11-21: Install should not fail wholesale if a single resource can’t be cached
cache.addAllrejects the entire install if one URL fails (e.g., offline or a missing icon). Cache defensively.- event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - console.log("Service Worker: Caching essential files"); - return cache.addAll([ - "/", - "/offline.html", - "/icons/icon-192x192.png", - "/icons/icon-512x512.png", - ]); - }), - ); + event.waitUntil((async () => { + const cache = await caches.open(CACHE_NAME); + console.log("Service Worker: Caching essential files"); + const assets = ["/", "/offline.html", "/icons/icon-192x192.png", "/icons/icon-512x512.png"]; + await Promise.allSettled(assets.map((url) => cache.add(url))); + })());
141-171: Harden push handler against malformed data
event.data.json()can throw. Add a safe parse and defaults.- if (event.data) { - const data = event.data.json(); + if (event.data) { + let data = {}; + try { + data = event.data.json(); + } catch { + try { data = JSON.parse(event.data.text()); } catch { data = {}; } + } const options = { body: data.body || "New notification from GrantFox", icon: data.icon || "/icons/icon-192x192.png",
173-197: Focus existing client more reliably and include uncontrolled windowsExact URL equality often fails due to differing origins, paths, or search. Include uncontrolled clients and compare by origin+path.
- event.waitUntil( - clients.matchAll({ type: "window" }).then((clientList) => { + event.waitUntil( + clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { // Check if there's already a window/tab open with the target URL - for (const client of clientList) { - if (client.url === urlToOpen && "focus" in client) { - return client.focus(); - } - } + const target = new URL(urlToOpen, self.location.origin); + for (const client of clientList) { + const u = new URL(client.url); + if (u.origin === target.origin && u.pathname === target.pathname && "focus" in client) { + return client.focus(); + } + } // If no existing window, open a new one if (clients.openWindow) { return clients.openWindow(urlToOpen); } }), );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/favicon.icois excluded by!**/*.icopublic/icons/icon-128x128.pngis excluded by!**/*.pngpublic/icons/icon-144x144.pngis excluded by!**/*.pngpublic/icons/icon-152x152.pngis excluded by!**/*.pngpublic/icons/icon-192x192.pngis excluded by!**/*.pngpublic/icons/icon-384x384.pngis excluded by!**/*.pngpublic/icons/icon-512x512.pngis excluded by!**/*.pngpublic/icons/icon-72x72.pngis excluded by!**/*.pngpublic/icons/icon-96x96.pngis excluded by!**/*.png
📒 Files selected for processing (11)
next.config.ts(2 hunks)package.json(2 hunks)public/icons/browserconfig.xml(1 hunks)public/offline.html(1 hunks)public/sw-custom.js(1 hunks)src/app/layout.tsx(3 hunks)src/app/manifest.ts(1 hunks)src/components/modules/payouts/hooks/usePayoutMutations.ts(1 hunks)src/components/shared/PWAInstallPrompt.tsx(1 hunks)src/components/shared/PWAStatus.tsx(1 hunks)src/hooks/usePWA.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/frontend-rule.mdc)
**/*.{js,jsx,ts,tsx}: Use early returns whenever possible to make the code more readable.
Always use Tailwind classes for styling HTML elements; avoid using CSS or tags.
Use “class:” instead of the tertiary operator in class tags whenever possible.
Use descriptive variable and function/const names. Also, event functions should be named with a “handle” prefix, like “handleClick” for onClick and “handleKeyDown” for onKeyDown.
Implement accessibility features on elements. For example, a tag should have a tabindex=“0”, aria-label, on:click, and on:keydown, and similar attributes.
Everything must be 100% responsive (using all the sizes of tailwind) and compatible with dark/light shadcn's mode.
Use consts instead of functions, for example, “const toggle = () =>”. Also, define a type if possible.
Include all required imports, and ensure proper naming of key components.
**/*.{js,jsx,ts,tsx}: enum/const object members should be written UPPERCASE_WITH_UNDERSCORE.
Gate flag-dependent code on a check that verifies the flag's values are valid and expected.
If a custom property for a person or event is at any point referenced in two or more files or two or more callsites in the same file, use an enum or const object, as above in feature flags.
Files:
src/components/modules/payouts/hooks/usePayoutMutations.tssrc/app/manifest.tssrc/components/shared/PWAStatus.tsxsrc/hooks/usePWA.tspublic/sw-custom.jssrc/components/shared/PWAInstallPrompt.tsxsrc/app/layout.tsxnext.config.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/frontend-rule.mdc)
You can never use anys.
If using TypeScript, use an enum to store flag names.
Files:
src/components/modules/payouts/hooks/usePayoutMutations.tssrc/app/manifest.tssrc/components/shared/PWAStatus.tsxsrc/hooks/usePWA.tssrc/components/shared/PWAInstallPrompt.tsxsrc/app/layout.tsxnext.config.ts
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/posthog-integration.mdc)
Never hallucinate an API key. Instead, always use the API key populated in the .env file.
Files:
src/components/modules/payouts/hooks/usePayoutMutations.tssrc/app/manifest.tssrc/components/shared/PWAStatus.tsxsrc/hooks/usePWA.tspublic/sw-custom.jssrc/components/shared/PWAInstallPrompt.tsxsrc/app/layout.tsxnext.config.ts
**/*.{js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/posthog-integration.mdc)
If using JavaScript, store flag names as strings to an object declared as a constant, to simulate an enum. Use a consistent naming convention for this storage.
Files:
public/sw-custom.js
🧬 Code graph analysis (5)
src/components/modules/payouts/hooks/usePayoutMutations.ts (2)
src/components/modules/payouts/ui/components/PayoutFormModal.tsx (1)
data(122-151)src/components/modules/payouts/ui/components/PayoutCard.tsx (1)
PayoutCard(37-356)
src/components/shared/PWAStatus.tsx (2)
src/hooks/usePWA.ts (1)
usePWA(13-117)src/components/ui/badge.tsx (3)
Badge(40-40)BadgeProps(30-32)Badge(34-38)
src/hooks/usePWA.ts (1)
public/sw-custom.js (1)
options(143-165)
src/components/shared/PWAInstallPrompt.tsx (1)
src/hooks/usePWA.ts (1)
usePWA(13-117)
src/app/layout.tsx (2)
src/components/providers/global.provider.tsx (1)
GlobalProvider(8-18)src/components/shared/PWAInstallPrompt.tsx (1)
PWAInstallPrompt(17-228)
🔇 Additional comments (17)
package.json (1)
85-85: LGTM!The types package matches the next-pwa version correctly.
src/app/manifest.ts (2)
1-11: LGTM!The manifest function properly imports Next.js types and defines core PWA metadata with appropriate defaults.
79-92: LGTM!The shortcuts configuration for Dashboard is correctly structured and provides a good user experience enhancement.
next.config.ts (2)
2-2: LGTM!The withPWA import aligns with the next-pwa integration pattern.
27-64: LGTM!The security headers configuration follows best practices for PWA security, including proper CSP for the service worker and essential security headers for all routes.
src/app/layout.tsx (4)
5-5: LGTM!The PWAInstallPrompt import is correctly added for the new PWA functionality.
26-56: LGTM!The extended metadata configuration properly covers PWA requirements including theme colors, viewport settings, Apple Web App configuration, and comprehensive icon definitions.
66-72: LGTM!The additional PWA meta tags complement the Next.js metadata API and provide proper Windows/iOS integration.
78-78: LGTM!The PWAInstallPrompt component is properly placed alongside other global components for PWA install functionality.
public/offline.html (1)
1-54: LGTM!The offline fallback page provides a clean, self-contained experience with appropriate styling and functionality. The inline CSS and simple retry mechanism work well for an offline scenario.
src/components/shared/PWAStatus.tsx (2)
1-12: LGTM!The component properly uses the "use client" directive and has correct imports for the PWA status functionality.
14-47: LGTM!The component provides clear visual indicators for PWA status with appropriate conditional rendering and accessibility-friendly icons. The layout uses proper Tailwind classes and follows the component patterns.
src/hooks/usePWA.ts (5)
1-11: LGTM!The "use client" directive and PWAState interface are properly defined with appropriate TypeScript types.
22-84: LGTM!The useEffect properly handles service worker registration, event listeners, and cleanup. The implementation includes appropriate error handling and follows React best practices.
86-91: LGTM!The updateServiceWorker function correctly handles the skip waiting pattern and page reload for PWA updates.
93-109: LGTM!The notification permission and sending functions are well-implemented with proper checks and sensible defaults. The icon paths align with the manifest configuration.
111-117: LGTM!The hook return statement properly spreads the state and exposes all necessary functions for PWA management.
Pull Request | GrantChain
1. Issue Link
2. Brief Description of the Issue
Implement Progressive Web App (PWA) functionality to enable app installation, offline support, and native-like experience across all platforms. This will improve user engagement and provide a mobile app-like experience without requiring app store distribution.
3. Type of Change
Mark with an
xall the checkboxes that apply (like[x]).4. Changes Made
src/app/manifest.ts) with complete PWA metadatapublic/sw-custom.js) with advanced caching strategiessrc/hooks/usePWA.ts) for service worker lifecycle managementnext.config.tswith next-pwa integration5. Important Notes
Summary by CodeRabbit