-
Notifications
You must be signed in to change notification settings - Fork 19
feat: Mobile Apps (iOS/Android) via PWA (Issue #165) #334
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: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "short_name": "CareSync", | ||
| "name": "CareSync Health Tracker", | ||
| "icons": [ | ||
| { | ||
| "src": "favicon.ico", | ||
| "sizes": "64x64 32x32 24x24 16x16", | ||
| "type": "image/x-icon" | ||
| }, | ||
| { | ||
| "src": "logo192.png", | ||
| "type": "image/png", | ||
| "sizes": "192x192" | ||
| }, | ||
| { | ||
| "src": "logo512.png", | ||
| "type": "image/png", | ||
| "sizes": "512x512" | ||
| } | ||
| ], | ||
| "start_url": ".", | ||
| "display": "standalone", | ||
| "theme_color": "#000000", | ||
| "background_color": "#ffffff" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,124 +1,76 @@ | ||
| /* eslint-disable no-restricted-globals */ | ||
|
|
||
| const CACHE_NAME = 'caresync-cache-v1'; | ||
|
|
||
| /** | ||
| * CareSync Service Worker — medicine reminder notifications & offline caching. | ||
| * | ||
| * Handles incoming `showNotification` calls from the main thread and | ||
| * reacts to notification clicks by navigating to the Medicine Tracker. | ||
| * Also caches static assets and API responses for offline use. | ||
| * | ||
| * Registered in `src/index.js` via `navigator.serviceWorker.register`. | ||
| * | ||
| * @file public/sw.js | ||
| */ | ||
|
|
||
| /** | ||
| * notificationclick — fired when the user taps/clicks a notification. | ||
| */ | ||
| globalThis.addEventListener('notificationclick', (event) => { | ||
| event.notification.close(); | ||
|
|
||
| const targetUrl = '/medicine-tracker'; | ||
| const CACHE_NAME = 'caresync-v1'; | ||
| const urlsToCache = [ | ||
| '/', | ||
| '/index.html', | ||
| '/manifest.json' | ||
| ]; | ||
|
|
||
| self.addEventListener('install', event => { | ||
| event.waitUntil( | ||
| globalThis.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { | ||
| // Try to focus an existing tab that already has the app open. | ||
| for (const client of clientList) { | ||
| const rawPath = new URL(client.url).pathname; | ||
| const clientPath = rawPath.length > 1 && rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath; | ||
| if (clientPath === targetUrl && 'focus' in client) { | ||
| return client.focus(); | ||
| } | ||
| } | ||
| // No matching tab — open a new one. | ||
| if (globalThis.clients.openWindow) { | ||
| return globalThis.clients.openWindow(targetUrl); | ||
| } | ||
| }) | ||
| caches.open(CACHE_NAME) | ||
| .then(cache => { | ||
| return cache.addAll(urlsToCache); | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| globalThis.addEventListener('install', (event) => { | ||
| globalThis.skipWaiting(); | ||
| self.addEventListener('fetch', event => { | ||
| if (event.request.method !== 'GET') return; | ||
|
|
||
| event.respondWith( | ||
| caches.match(event.request) | ||
| .then(response => { | ||
| if (response) { | ||
| return response; | ||
| } | ||
|
|
||
| return fetch(event.request).then(networkResponse => { | ||
|
Check failure on line 27 in public/sw.js
|
||
| if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') { | ||
|
Check warning on line 28 in public/sw.js
|
||
| return networkResponse; | ||
| } | ||
| const responseToCache = networkResponse.clone(); | ||
| caches.open(CACHE_NAME).then(cache => { | ||
| cache.put(event.request, responseToCache); | ||
| }); | ||
| return networkResponse; | ||
| }).catch(() => { | ||
| // Fallback if needed, e.g., return offline page | ||
| }); | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| /** | ||
| * activate — claim all open clients immediately and clean up old caches. | ||
| */ | ||
| globalThis.addEventListener('activate', (event) => { | ||
| self.addEventListener('activate', event => { | ||
| const cacheWhitelist = [CACHE_NAME]; | ||
| event.waitUntil( | ||
| caches.keys().then((cacheNames) => { | ||
| caches.keys().then(cacheNames => { | ||
| return Promise.all( | ||
| cacheNames.map((cacheName) => { | ||
| if (cacheName !== CACHE_NAME) { | ||
| cacheNames.map(cacheName => { | ||
| if (cacheWhitelist.indexOf(cacheName) === -1) { | ||
|
Check warning on line 49 in public/sw.js
|
||
| return caches.delete(cacheName); | ||
| } | ||
| }) | ||
| ); | ||
| }).then(() => globalThis.clients.claim()) | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| /** | ||
| * fetch — intercept network requests for offline caching. | ||
| */ | ||
| globalThis.addEventListener('fetch', (event) => { | ||
| const { request } = event; | ||
|
|
||
| // Handle API GET requests (Network First strategy) | ||
| if (request.url.includes('/api/') && request.method === 'GET') { | ||
| event.respondWith( | ||
| fetch(request) | ||
| .then((response) => { | ||
| if (response.status === 200) { | ||
| const responseClone = response.clone(); | ||
| caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); | ||
| } | ||
| return response; | ||
| }) | ||
| .catch(() => { | ||
| return caches.match(request).then((cachedResponse) => { | ||
| if (cachedResponse) return cachedResponse; | ||
| return new Response( | ||
| JSON.stringify({ error: 'Offline', message: 'No cached data available' }), | ||
| { status: 503, headers: { 'Content-Type': 'application/json' } } | ||
| ); | ||
| }); | ||
| }) | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Skip other non-GET requests (handled by axios interceptor in api.js) | ||
| if (request.method !== 'GET') return; | ||
|
|
||
| // Static Assets (Network First with Cache Fallback) | ||
| event.respondWith( | ||
| fetch(request) | ||
| .then((response) => { | ||
| if (response.status === 200 || response.type === 'opaque') { | ||
| const responseClone = response.clone(); | ||
| caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone)); | ||
| self.addEventListener('notificationclick', event => { | ||
| event.notification.close(); | ||
|
|
||
| event.waitUntil( | ||
| clients.matchAll({ type: 'window' }).then(windowClients => { | ||
| // Check if there is already a window/tab open with the target URL | ||
| for (let i = 0; i < windowClients.length; i++) { | ||
| const client = windowClients[i]; | ||
| if (client.url.includes(self.registration.scope) && 'focus' in client) { | ||
| return client.focus(); | ||
| } | ||
| return response; | ||
| }) | ||
| .catch(() => { | ||
| return caches.match(request).then((cachedResponse) => { | ||
| if (cachedResponse) return cachedResponse; | ||
|
|
||
| // For navigation requests, fallback to index.html if offline | ||
| if (request.mode === 'navigate') { | ||
| return caches.match('/index.html').then(idxResp => { | ||
| if (idxResp) return idxResp; | ||
| return new Response('Offline. App could not be loaded.', { status: 503, headers: { 'Content-Type': 'text/html' } }); | ||
| }); | ||
| } | ||
|
|
||
| // Generic fallback to avoid TypeError: Failed to convert value to 'Response' | ||
| return new Response('', { status: 503, statusText: 'Service Unavailable (Offline)' }); | ||
| }); | ||
| }) | ||
| } | ||
|
Check warning on line 69 in public/sw.js
|
||
| // If no window is open, open a new one | ||
| if (clients.openWindow) { | ||
| return clients.openWindow('/'); | ||
| } | ||
| }) | ||
| ); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.