this repo has no description
1import { useState, useEffect, useCallback } from 'react';
2import { apiClient } from '../api/client';
3
4interface PushPreferences {
5 reminder_enabled: boolean;
6 reminder_time: string; // "HH:MM" in UTC
7}
8
9function urlBase64ToUint8Array(base64String: string): Uint8Array {
10 const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
11 const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
12 const raw = atob(base64);
13 const arr = new Uint8Array(raw.length);
14 for (let i = 0; i < raw.length; i++) {
15 arr[i] = raw.charCodeAt(i);
16 }
17 return arr;
18}
19
20export function localTimeToUtc(localTime: string): string {
21 const [hours, minutes] = localTime.split(':').map(Number);
22 const now = new Date();
23 now.setHours(hours, minutes, 0, 0);
24 const utcH = now.getUTCHours().toString().padStart(2, '0');
25 const utcM = now.getUTCMinutes().toString().padStart(2, '0');
26 return `${utcH}:${utcM}`;
27}
28
29export function utcTimeToLocal(utcTime: string): string {
30 const [hours, minutes] = utcTime.split(':').map(Number);
31 const now = new Date();
32 now.setUTCHours(hours, minutes, 0, 0);
33 const localH = now.getHours().toString().padStart(2, '0');
34 const localM = now.getMinutes().toString().padStart(2, '0');
35 return `${localH}:${localM}`;
36}
37
38export function usePushNotifications() {
39 const [permission, setPermission] = useState<NotificationPermission>(
40 typeof window !== 'undefined' && 'Notification' in window
41 ? Notification.permission
42 : 'denied',
43 );
44 const [preferences, setPreferences] = useState<PushPreferences | null>(null);
45 const [loading, setLoading] = useState(true);
46
47 useEffect(() => {
48 apiClient
49 .get<PushPreferences>('/api/push/preferences')
50 .then(setPreferences)
51 .catch(() => {})
52 .finally(() => setLoading(false));
53 }, []);
54
55 const subscribe = useCallback(async () => {
56 if (!('Notification' in window) || !('serviceWorker' in navigator)) return;
57
58 const perm = await Notification.requestPermission();
59 setPermission(perm);
60 if (perm !== 'granted') return;
61
62 const { vapid_public_key } = await apiClient.get<{ vapid_public_key: string }>(
63 '/api/push/vapid-key',
64 );
65
66 const applicationServerKey = urlBase64ToUint8Array(vapid_public_key);
67
68 const registration = await navigator.serviceWorker.ready;
69 const subscription = await registration.pushManager.subscribe({
70 userVisibleOnly: true,
71 applicationServerKey: applicationServerKey.buffer as ArrayBuffer,
72 });
73
74 const json = subscription.toJSON();
75 await apiClient.post('/api/push/subscribe', {
76 endpoint: json.endpoint,
77 p256dh: json.keys?.p256dh,
78 auth: json.keys?.auth,
79 });
80 }, []);
81
82 const unsubscribe = useCallback(async () => {
83 if (!('serviceWorker' in navigator)) return;
84
85 try {
86 const registration = await navigator.serviceWorker.ready;
87 const subscription = await registration.pushManager.getSubscription();
88 if (subscription) {
89 await subscription.unsubscribe();
90 }
91 } catch {
92 // ignore errors
93 }
94 await apiClient.delete('/api/push/subscribe');
95 }, []);
96
97 const updatePreferences = useCallback(async (prefs: PushPreferences) => {
98 await apiClient.put('/api/push/preferences', prefs);
99 setPreferences(prefs);
100 }, []);
101
102 return { permission, preferences, loading, subscribe, unsubscribe, updatePreferences };
103}