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:
parent
dcb6657966
commit
27e8344e7a
55
public/sw.js
55
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',
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue