feat: Add background location sync via service worker

- Add Background Sync API support for location sync when coming back online
- Add silent push notifications for requesting location updates
- Add useServiceWorkerMessages hook for handling SW messages
- Connect service worker to location sharing for background updates

This enables:
- Silent location requests via push without showing notifications
- Automatic location sync when device comes back online
- Service worker communication for background location updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-29 01:15:17 +01:00
parent dcb6657966
commit 27e8344e7a
3 changed files with 141 additions and 1 deletions

View File

@ -1,5 +1,6 @@
// rMaps Service Worker for Push Notifications
// rMaps Service Worker for Push Notifications & Background Location
const CACHE_NAME = 'rmaps-v1';
const SYNC_TAG = 'rmaps-location-sync';
// Install event - cache essential assets
self.addEventListener('install', (event) => {
@ -22,6 +23,47 @@ self.addEventListener('activate', (event) => {
self.clients.claim();
});
// Background Sync - triggered when device comes back online
self.addEventListener('sync', (event) => {
console.log('[SW] Sync event:', event.tag);
if (event.tag === SYNC_TAG) {
event.waitUntil(syncLocation());
}
});
// Sync location to server when coming back online
async function syncLocation() {
try {
// Get stored location data from IndexedDB or localStorage via client
const clients = await self.clients.matchAll({ type: 'window' });
for (const client of clients) {
// Ask the client to send its current location
client.postMessage({ type: 'REQUEST_LOCATION_SYNC' });
}
console.log('[SW] Location sync requested from clients');
} catch (error) {
console.error('[SW] Location sync failed:', error);
}
}
// Request location from any available client
async function requestLocationFromClient() {
const clients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true
});
if (clients.length > 0) {
// Send message to first available client to get location
clients[0].postMessage({ type: 'REQUEST_LOCATION_UPDATE' });
return true;
}
return false;
}
// Push event - handle incoming push notifications
self.addEventListener('push', (event) => {
console.log('[SW] Push received:', event);
@ -33,6 +75,7 @@ self.addEventListener('push', (event) => {
badge: '/icon-192.png',
tag: 'rmaps-notification',
data: {},
silent: false,
};
if (event.data) {
@ -44,6 +87,16 @@ self.addEventListener('push', (event) => {
}
}
// Handle silent push - request location update without showing notification
if (data.silent || data.data?.type === 'location_request') {
event.waitUntil(
requestLocationFromClient().then((sent) => {
console.log('[SW] Silent push - location request sent:', sent);
})
);
return;
}
const options = {
body: data.body,
icon: data.icon || '/icon-192.png',

View File

@ -5,6 +5,7 @@ import { useParams, useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
import { useRoom } from '@/hooks/useRoom';
import { useLocationSharing } from '@/hooks/useLocationSharing';
import { useServiceWorkerMessages } from '@/hooks/useServiceWorkerMessages';
import ParticipantList from '@/components/room/ParticipantList';
import RoomHeader from '@/components/room/RoomHeader';
import ShareModal from '@/components/room/ShareModal';
@ -112,12 +113,31 @@ export default function RoomPage() {
currentLocation,
startSharing,
stopSharing,
requestUpdate,
} = useLocationSharing({
onLocationUpdate: handleLocationUpdate,
updateInterval: 5000,
highAccuracy: true,
});
// Service worker messages for background location sync
useServiceWorkerMessages({
onLocationRequest: () => {
// Silent push notification requested location update
console.log('Service worker requested location update');
if (isSharing) {
requestUpdate();
}
},
onLocationSync: () => {
// Device came back online, sync location
console.log('Service worker requested location sync');
if (isSharing) {
requestUpdate();
}
},
});
// Restore last known location immediately when connected
const hasRestoredLocationRef = useRef(false);
useEffect(() => {

View File

@ -0,0 +1,67 @@
'use client';
import { useEffect, useCallback, useRef } from 'react';
interface UseServiceWorkerMessagesOptions {
onLocationRequest?: () => void;
onLocationSync?: () => void;
}
/**
* Hook to handle messages from the service worker
* Used for background location sync and silent push notifications
*/
export function useServiceWorkerMessages(options: UseServiceWorkerMessagesOptions = {}) {
const optionsRef = useRef(options);
optionsRef.current = options;
useEffect(() => {
if (!('serviceWorker' in navigator)) {
return;
}
const handleMessage = (event: MessageEvent) => {
console.log('[App] Service worker message:', event.data);
switch (event.data?.type) {
case 'REQUEST_LOCATION_UPDATE':
// Service worker is requesting a location update (from silent push)
optionsRef.current.onLocationRequest?.();
break;
case 'REQUEST_LOCATION_SYNC':
// Service worker wants to sync location (device came back online)
optionsRef.current.onLocationSync?.();
break;
}
};
navigator.serviceWorker.addEventListener('message', handleMessage);
return () => {
navigator.serviceWorker.removeEventListener('message', handleMessage);
};
}, []);
// Register for background sync
const registerBackgroundSync = useCallback(async () => {
if (!('serviceWorker' in navigator) || !('SyncManager' in window)) {
console.log('[App] Background sync not supported');
return false;
}
try {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('rmaps-location-sync');
console.log('[App] Background sync registered');
return true;
} catch (error) {
console.error('[App] Background sync registration failed:', error);
return false;
}
}, []);
return {
registerBackgroundSync,
};
}