From 27e8344e7a324ea43b8e621398fbeb21287188f3 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 29 Dec 2025 01:15:17 +0100 Subject: [PATCH] feat: Add background location sync via service worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- public/sw.js | 55 +++++++++++++++++++++- src/app/[slug]/page.tsx | 20 ++++++++ src/hooks/useServiceWorkerMessages.ts | 67 +++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useServiceWorkerMessages.ts diff --git a/public/sw.js b/public/sw.js index e64bfe3..1efa617 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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', diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 14ba3b8..d7b5893 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -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(() => { diff --git a/src/hooks/useServiceWorkerMessages.ts b/src/hooks/useServiceWorkerMessages.ts new file mode 100644 index 0000000..af5ee64 --- /dev/null +++ b/src/hooks/useServiceWorkerMessages.ts @@ -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, + }; +}