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 CACHE_NAME = 'rmaps-v1';
|
||||||
|
const SYNC_TAG = 'rmaps-location-sync';
|
||||||
|
|
||||||
// Install event - cache essential assets
|
// Install event - cache essential assets
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
|
|
@ -22,6 +23,47 @@ self.addEventListener('activate', (event) => {
|
||||||
self.clients.claim();
|
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
|
// Push event - handle incoming push notifications
|
||||||
self.addEventListener('push', (event) => {
|
self.addEventListener('push', (event) => {
|
||||||
console.log('[SW] Push received:', event);
|
console.log('[SW] Push received:', event);
|
||||||
|
|
@ -33,6 +75,7 @@ self.addEventListener('push', (event) => {
|
||||||
badge: '/icon-192.png',
|
badge: '/icon-192.png',
|
||||||
tag: 'rmaps-notification',
|
tag: 'rmaps-notification',
|
||||||
data: {},
|
data: {},
|
||||||
|
silent: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.data) {
|
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 = {
|
const options = {
|
||||||
body: data.body,
|
body: data.body,
|
||||||
icon: data.icon || '/icon-192.png',
|
icon: data.icon || '/icon-192.png',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useParams, useRouter } from 'next/navigation';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useRoom } from '@/hooks/useRoom';
|
import { useRoom } from '@/hooks/useRoom';
|
||||||
import { useLocationSharing } from '@/hooks/useLocationSharing';
|
import { useLocationSharing } from '@/hooks/useLocationSharing';
|
||||||
|
import { useServiceWorkerMessages } from '@/hooks/useServiceWorkerMessages';
|
||||||
import ParticipantList from '@/components/room/ParticipantList';
|
import ParticipantList from '@/components/room/ParticipantList';
|
||||||
import RoomHeader from '@/components/room/RoomHeader';
|
import RoomHeader from '@/components/room/RoomHeader';
|
||||||
import ShareModal from '@/components/room/ShareModal';
|
import ShareModal from '@/components/room/ShareModal';
|
||||||
|
|
@ -112,12 +113,31 @@ export default function RoomPage() {
|
||||||
currentLocation,
|
currentLocation,
|
||||||
startSharing,
|
startSharing,
|
||||||
stopSharing,
|
stopSharing,
|
||||||
|
requestUpdate,
|
||||||
} = useLocationSharing({
|
} = useLocationSharing({
|
||||||
onLocationUpdate: handleLocationUpdate,
|
onLocationUpdate: handleLocationUpdate,
|
||||||
updateInterval: 5000,
|
updateInterval: 5000,
|
||||||
highAccuracy: true,
|
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
|
// Restore last known location immediately when connected
|
||||||
const hasRestoredLocationRef = useRef(false);
|
const hasRestoredLocationRef = useRef(false);
|
||||||
useEffect(() => {
|
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